diff --git a/CLAUDE.md b/CLAUDE.md index 75e32bd..f51619b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -122,9 +122,16 @@ QTorres/ **Fase 3 — Sub-VIs y extensibilidad (en curso):** - ~~#17 Sub-VI con connector pane~~ ✅ (pin-based connector, compile-subvi-call, runner carga contextos, btn-run sincronizado) -- #18 Librería .qlib +- ~~#18 Librería .qlib~~ ✅ (load-qlib, find-qlibs, paleta integrada, ejemplo math.qlib, 482 tests PASS) +- ~~#64 FP como ventana maestra~~ ✅ (FP=blocking master, BD=no-wait slave, Ctrl+E toggle, títulos sincronizados, current-file en app-model) +- ~~#65 Scroll en BD y FP~~ ✅ (ventanas fijas 900x600, scroll wheel + click scrollbar, límites por contenido real) -**Próximo paso:** #18 Librería .qlib +**Fase 5 — UX y gestión de proyectos (planificado):** +- Splash / Welcome screen (Create New VI, Open Existing, proyectos recientes) +- Project Explorer con formato .qproj (árbol de ficheros, gestión de dependencias) +- Depende de: .qlib (#18) ✅ y FP como ventana maestra (#64) ✅ + +**Próximo paso:** Fase 4 (hardware) o Fase 5 (UX) ## Decisiones técnicas clave @@ -275,8 +282,10 @@ Estrategia QA: tests con cada feature nueva, no sesión QA dedicada. Spec visual: cada tipo implementa su aspecto según `docs/visual-spec.md`. **Fase 3 — Sub-VIs y extensibilidad:** -- #17 Sub-VI con connector pane -- #18 Librería .qlib +- #17 Sub-VI con connector pane ✅ +- #18 Librería .qlib ✅ +- #64 FP como ventana maestra — BD bajo demanda (Ctrl+E) ✅ +- ~~#65 Scroll en BD y FP~~ ✅ (ventanas fijas 900x600, scrollbars draw-based, límites por contenido) **Fase 4 — Hardware:** - #19 SCPI sobre TCP/IP (Keysight por red) @@ -285,6 +294,10 @@ Spec visual: cada tipo implementa su aspecto según `docs/visual-spec.md`. - #22 TCP/IP genérico (Modbus TCP, protocolos propios) - #23 DAQ analógico (comedi/libcomedi) +**Fase 5 — UX y gestión de proyectos:** +- Splash / Welcome screen (Create New VI, Open Existing, proyectos recientes) +- Project Explorer con formato .qproj (árbol de proyecto, gestión de dependencias) + ## Ollama MCP — Delegación de tareas a modelo local QTorres tiene un MCP server que conecta con Ollama (modelo local). Ollama tiene cargado automáticamente CLAUDE.md y el skill de Red-Lang como contexto del proyecto. diff --git a/docs/GTK_ISSUES.md b/docs/GTK_ISSUES.md index f0af223..0cad031 100644 --- a/docs/GTK_ISSUES.md +++ b/docs/GTK_ISSUES.md @@ -92,6 +92,9 @@ Cuando Red migre a 64-bit, este problema desaparece. QTorres debe seguir ese roa | GTK-008 `request-file/save` abre diálogo de carpetas | — | Workaround: diálogo VID propio | | GTK-009 `request-file` no permite controlar tamaño | — | Posible: file browser VID propio | | GTK-010 `on-change` de field queda enganchado tras Run | — | Issue anlaco/QTorres#49 | +| GTK-014 `face/size` flip-flop CSD↔cliente tras alt+tab | — | Workaround: ventanas fijas 900x600 sin resize (Issue #65) | +| GTK-015 Tab crashea navegación foco en window con solo `base` | — | Pendiente de crear — no fatal | +| GTK-016 Access violation en show/draw bajo maximize/resize | — | Crítico — sin workaround user-land | --- @@ -115,6 +118,97 @@ La causa probable es que Red/View no llama a `gtk_window_set_transient_for()` o --- +### GTK-014: `face/size` reporta dos interpretaciones distintas según el estado de foco — flip-flop tras alt+tab / maximize / restore + +**Severidad:** Alta +**Impacto en QTorres:** El canvas del BD y del FP se dimensionaban mal tras alt+tab o restore de maximize, saliéndose por la derecha y/o abajo de la ventana, o quedándose demasiado pequeño con huecos visibles. + +**Descripción:** +En GTK3 con CSD (Client-Side Decorations), `face/size` de una ventana reporta **dos valores distintos para el mismo estado visual**, y cambia entre ellos tras eventos de foco sin intervención del usuario: + +- **Modo CSD** (inicial y tras restore): `face/size = área cliente + header bar + sombras` +- **Modo cliente** (tras primer alt+tab o focus cycle): `face/size = área cliente + header bar` (sin sombras) + +El valor en modo cliente es `~98x98 px menor` que en modo CSD para la misma ventana visual. No hay API en Red/View para saber en qué modo está GTK. + +**Ejemplo capturado** (pantalla 1366x768, ventana maximizada): + +``` +#21 on-resize 1464x836 ← maximizar, modo CSD (shadows incluidas) +#22 on-time 1464x836 +#23 on-unfocus 1366x738 ← alt+tab, GTK cambia a modo cliente (-98x-98) +#28 on-resize 663x486 ← restore, modo cliente (747-98, 584-98) +#30 on-resize 747x584 ← mismo estado, GTK vuelve a modo CSD (+98x+98) +``` + +**Workaround implementado (Issue #65):** Ventanas de tamaño fijo (900x600) sin `flags: [resize]`. Al no haber redimensionado, el flip CSD↔cliente no afecta al layout — los canvas tienen tamaño fijo calculado contra el spec de la ventana, no contra `face/size`. + +La detección bidireccional del flip fue explorada y descartada: los deltas -98x-98 durante maximize son indistinguibles de un flip legítimo por alt+tab, y la lógica de corrección se volvía inestable. Ver `tests/test-overhead.red` para el diagnóstico completo. + +**Test reproducible:** `tests/test-overhead.red` — con logging a `/tmp/test-overhead.log` para capturar la secuencia de eventos. + +--- + +### GTK-015: Pulsar `Tab` en ventana con solo `base` face crashea en navegación de foco + +**Severidad:** Media (no fatal) +**Impacto en QTorres:** Si el usuario pulsa Tab con foco en el canvas del BD o FP, aparece un error en stderr. La aplicación **no muere** — el event loop continúa funcionando normalmente. + +**Descripción:** +Al pulsar Tab en una ventana cuyo `pane` solo contiene faces de tipo `base` (no focusables), el handler interno de navegación de foco de Red/View intenta recorrer `p/parent/pane` y falla porque `parent` es `none`: + +``` +*** Script Error: path p/parent/pane is not valid for none! type +*** Where: eval-path +*** Near : handler face event +*** Stack: view do-events do-safe +``` + +**Hallazgos del diagnóstico:** + +1. **`on-key` recibe el Tab** (`event/key = #"^-"`) — el evento llega al user-land antes del crash. +2. **`return 'done` desde `on-key` NO previene** el handler interno — Red/View lo ejecuta igualmente. +3. **Añadir un `field` al pane como "tab-sink"** no evita el crash. Si es `visible?: false` GTK emite `gtk_widget_event: WIDGET_REALIZED_FOR_EVENT failed` porque el widget no está realizado. Si es visible pero off-screen, el crash sigue produciéndose en un handler distinto. +4. **`set-focus tab-sink` en `on-create`** falla porque los widgets hijos aún no están realizados en ese momento. +5. **El crash es no-fatal** — el event loop sigue vivo y los siguientes eventos de teclado se procesan normalmente. + +**Workaround temporal:** Ninguno limpio desde user-land. Se acepta como limitación conocida de Red/View GTK. El BD/FP de QTorres no usa Tab como interacción normal. + +**Test reproducible:** `tests/test-overhead.red` — pulsar Tab muestra el error repetidamente en stderr pero la aplicación sigue funcionando. + +--- + +### GTK-016: Access violation en `show`/`draw` bajo maximize/resize repetidos + +**Severidad:** Crítica +**Impacto en QTorres:** Bajo presión de eventos de resize (maximize/restore rápido, o drag agresivo del borde) el runtime de Red/View genera un `*** Runtime Error 1: access violation` nativo en una dirección dentro del runtime (ej. `at: 0809DC91h`). En un caso observado el crash arrastró al sistema entero hasta colgarlo. + +**Descripción:** +El crash ocurre esporádicamente al combinar: +- Modificaciones de `face/size` desde un handler (`on-time`) +- Llamadas a `show` sobre el face hijo (base con Draw) +- Eventos GTK concurrentes de maximize/restore/focus + +La pila de ejecución nunca llega al user-land — es un `access violation` en memoria nativa, probablemente en el path de actualización del widget GTK desde el binding de Red. No hay forma de capturarlo con `try`/`catch`: es segfault puro. + +**Hallazgos del diagnóstico:** + +1. **Intermitente** — no se reproduce de forma determinista. Requiere varios ciclos de maximize/restore seguidos. +2. **No depende de la lógica user-land** — se reproduce con el test simplificado `test-overhead.red` que no hace flip detection ni manipula estado. +3. **Peligroso** — en un caso concreto arrastró al sistema entero (no solo a la app) y obligó a reiniciar el equipo. +4. **No hay workaround** — cualquier estrategia que implique `show` tras cambiar `face/size` es vulnerable. + +**Workaround temporal:** Ninguno conocido desde user-land. Posibles mitigaciones a investigar: +- Diferir `show` con un timer adicional tras el resize +- Usar `show/with` o `show face/pane` en lugar de `show child` +- Evitar modificar `face/size` dentro del handler y hacerlo en un tick posterior + +**Siguiente paso:** Caso mínimo reproducible para bug report upstream a Red-Lang. Mientras tanto, QTorres debe asumir que el resize agresivo puede matar la app. + +**Test reproducible:** `tests/test-overhead.red` — maximize/restore repetido acaba disparando el crash en una fracción de los intentos. + +--- + ### GTK-010: `on-change` de field nativo queda enganchado tras ejecutar Run **Severidad:** Media diff --git a/docs/decisiones.md b/docs/decisiones.md index c8f9ea2..21d65f6 100644 --- a/docs/decisiones.md +++ b/docs/decisiones.md @@ -1072,3 +1072,48 @@ _err: scpi-read instrument _err | Error cluster desde Fase 2 | Complejidad prematura. Sin hardware, no hay errores reales que propagar | | Solo `try/catch` global | No permite al usuario ver qué nodo falló ni tomar decisiones en el diagrama | | Ignorar errores (solo `print`) | Inaceptable para producción industrial | + +--- + +## DT-030: UI Framework — Red/View + Draw con capa QT-Widgets propia + +**Fecha:** 2026-04-10 +**Estado:** Adoptada + +**Contexto:** QTorres necesita un editor visual con nodos arrastrables, wires, scrollbars, controles custom, inline text editing, tree views (project explorer) y más. Red/View proporciona ventanas y eventos, Draw proporciona renderizado 2D, pero no hay widget toolkit intermedio. Se evaluó si construir sobre Red/View+Draw, GTK directo, o Qt. + +**Alternativas evaluadas:** + +| Opción | Ventajas | Inconvenientes | +|--------|----------|----------------| +| **Red/View + Draw** (actual) | Todo en Red (DT-001), binario < 1 MB, multiplataforma, control total | Cada widget hay que construirlo desde cero, bugs GTK, no hay accessibility | +| **GTK (via FFI/C)** | Widgets nativos maduros, TreeView, ScrolledWindow, accessibility | Rompe DT-001, solo nativo en Linux, runtime pesado en Win/macOS, el canvas custom sigue siendo necesario | +| **Qt (C++/Python)** | QGraphicsScene resuelve el canvas, toolkit más completo que existe, multiplataforma real | Rompe DT-001 completamente, 50-100 MB de runtime, Red relegado a lenguaje del .qvi, no del editor | + +**Decisión:** Construir sobre Red/View + Draw, formalizando progresivamente una capa intermedia (QT-Widgets). + +**Arquitectura objetivo:** + +``` +Red/View (ventanas + event loop) + └── Draw (renderizado 2D) + └── QT-Widgets (capa propia: hit-test, scroll, controles Draw-based) + └── QTorres UI (canvas, panel, diálogos, project explorer) +``` + +**Razones:** + +1. **El canvas del diagrama es custom sí o sí.** Incluso con Qt/QGraphicsScene, los nodos QTorres, los wires con tipado por color, las estructuras de control y el connector pane necesitan renderizado propio. El 80% de la complejidad no se ahorra con un toolkit externo. + +2. **Identidad del proyecto.** "Todo en Red, un binario < 1 MB, sin dependencias" es la propuesta de valor que diferencia a QTorres de LabVIEW. Meter Qt o GTK la destruye. + +3. **Ya estamos construyendo el framework.** canvas-render.red (932 líneas), panel-render.red (411 líneas), el hit-testing en canvas.red — eso ya ES un framework UI custom, solo falta formalizarlo. + +4. **Los widgets necesarios son pocos.** Scrollbar, text input inline, tree view, tabs. No necesitamos un toolkit genérico de 200 widgets. + +**Plan de formalización:** + +- **Fases 3-4:** Seguir construyendo widgets ad-hoc (scroll, resize) dentro de los módulos existentes. No extraer todavía. +- **Fase 5+:** Cuando lleguen inline text editing, property panels y project explorer, extraer QT-Widgets como módulo en `src/ui/widgets/`. Widgets candidatos: scrollbar, text-input, tree-view, tab-bar. + +**Plan B:** Si Red se estanca (bugs GTK sin arreglar en 1-2 años, 64-bit no llega), migrar el editor a PyQt/PySide manteniendo Red como lenguaje del código generado (.qvi). El formato .qvi y el compilador no cambian. diff --git a/docs/plan.md b/docs/plan.md index 21de5af..4d9597a 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -157,11 +157,20 @@ Los controles de entrada se convierten en `field` editables. Los indicadores de ## Fase 3 — Sub-VIs y extensibilidad -- [ ] Connector pane: definir entradas/salidas de un VI para usarlo como bloque (#18) -- [ ] Compilador genera `func` Red para sub-VIs (DT-006, DT-009) -- [ ] Un .qvi con connector pane se puede usar como bloque en otro .qvi -- [ ] `.qlib`: librería de bloques con `context` Red para namespacing +### Sub-VIs +- [x] Connector pane: definir entradas/salidas de un VI para usarlo como bloque (#17) ✅ +- [x] Compilador genera `func` Red para sub-VIs (DT-006, DT-009) ✅ +- [x] Un .qvi con connector pane se puede usar como bloque en otro .qvi ✅ + +### Librería +- [ ] `.qlib`: librería de bloques con `context` Red para namespacing (#18) - [ ] Paleta de bloques extensible por el usuario + +### UX — Modelo de ventanas LabVIEW +- [ ] FP como ventana maestra — BD se abre bajo demanda con Ctrl+E (#64) +- [ ] Ventanas redimensionables con scroll horizontal y vertical (#65) + +### Herramientas - [ ] Depurador con sondas en wires (ver valor en ejecución) - [ ] Exportar a ejecutable (compilación Red nativa a binario) @@ -195,6 +204,26 @@ Esta fase es esencial para el público objetivo (mismo que LabVIEW: ingeniería --- +## Fase 5 — Experiencia de usuario y gestión de proyectos + +### Splash / Welcome screen +- [ ] Pantalla de bienvenida al lanzar QTorres (Create New VI, Open Existing, proyectos recientes) +- [ ] Depende de que exista el concepto de proyecto (.qproj) o al menos .qlib (#18) + +### Project Explorer (.qproj) +- [ ] Formato `.qproj`: fichero de proyecto que agrupa VIs, sub-VIs, librerías y targets +- [ ] Ventana Project Explorer con árbol de ficheros del proyecto (equivalente al .lvproj de LabVIEW) +- [ ] Abrir un .qproj carga el árbol y muestra el explorer (doble clic en un VI abre su FP) +- [ ] Gestión de dependencias entre VIs y librerías dentro del proyecto +- [ ] Depende de: .qlib (#18), FP como ventana maestra (#64) + +### Notas +- El splash screen tiene sentido cuando haya algo que "abrir" — un .qproj o al menos historial de .qvi recientes +- El Project Explorer es una feature grande que requiere .qlib resuelto primero +- El modelo LabVIEW es: splash → project explorer → doble clic VI → FP → Ctrl+E → BD + +--- + ## Hitos clave | Hito | Descripción | Fase | @@ -205,9 +234,13 @@ Esta fase es esencial para el público objetivo (mismo que LabVIEW: ingeniería | Tipo booleano | Wire verde, LED control/indicator | 2 ✅ | | Tipos completos | Boolean, string, array, cluster en wires | 2 | | Estructuras de control | Bucles y condicionales en el diagrama | 2 | -| Sub-VIs | VIs reutilizables como bloques con connector | 3 | +| Sub-VIs | VIs reutilizables como bloques con connector | 3 ✅ | +| FP como master | FP ventana principal, BD bajo demanda | 3 | +| Resize + scroll | Ventanas redimensionables con scrollbars | 3 | | Primera medida real | Controlar un Keysight desde QTorres | 4 | | DAQ completo | Adquisición continua con tarjeta o Arduino | 4 | +| Welcome screen | Splash con Create/Open al lanzar QTorres | 5 | +| Project Explorer | Árbol de proyecto .qproj con gestión de VIs | 5 | --- diff --git a/docs/tipos-de-fichero.md b/docs/tipos-de-fichero.md index a72e723..685d5ba 100644 --- a/docs/tipos-de-fichero.md +++ b/docs/tipos-de-fichero.md @@ -324,37 +324,55 @@ qproj [ ### `.qlib` — Librería -Una librería agrupa VIs y primitivas bajo un namespace. Puede contener tanto `.qvi` como `.qprim`. +Una librería agrupa VIs bajo un namespace. Es un **directorio** con un manifiesto `qlib.red` y los `.qvi` miembros. Cada miembro debe tener un `connector:` para poder usarse como sub-VI. +**Estructura:** +``` +proyecto/ + math.qlib ; manifiesto (fichero de texto) + math/ + add.qvi ; sub-VI con connector + subtract.qvi ; sub-VI con connector +``` + +**Formato del fichero `math.qlib`:** ```red qlib [ - version: 1 - name: "math" - + name: "math" + version: 1 + description: "Operaciones matematicas basicas" members: [ - %add.qprim - %subtract.qprim - %interpolate.qvi - %fft.qvi + %math/add.qvi + %math/subtract.qvi ] ] ``` -Código generado al cargar la librería: +**Comportamiento en QTorres:** +- La paleta del editor detecta automáticamente `.qlib` en el directorio de trabajo +- Los VIs de la librería aparecen en sección "Librerías" de la paleta con etiqueta `nombre-lib/vi` +- Al insertar un VI de librería se crea un nodo subvi igual que con cualquier sub-VI +- Al compilar, el compilador emite `#include` selectivo (solo los miembros usados) +**Código generado en el caller (usa `#include` selectivo):** ```red -math: context [ - do %math/add.qprim ; incrusta código de la primitiva - do %math/subtract.qprim - do %math/interpolate.qvi ; define math/interpolate - do %math/fft.qvi ; define math/fft -] - -; Uso desde otro VI: -math/interpolate datos frecuencia -math/fft señal +_saved-qtorres-runtime: value? 'qtorres-runtime +qtorres-runtime: true +#include %math.qlib/add.qvi ; solo los miembros usados +#include %math.qlib/subtract.qvi +if not _saved-qtorres-runtime [unset 'qtorres-runtime] + +; Llamadas con la convención nombre-context/exec: +resultado-suma: add/exec A B +resultado-resta: subtract/exec A B ``` +**Instalación:** +- Local al proyecto: copiar el directorio `.qlib` junto al `.qvi` principal +- Global del usuario: copiar a `~/.qtorres/libs/` (pendiente de implementar) + +**Ver ejemplo:** `examples/math.qlib` + `examples/math/` + `examples/usa-libreria.qvi` + ### `.qctl` — Type definition ```red diff --git a/examples/math.qlib b/examples/math.qlib new file mode 100644 index 0000000..6f59fad --- /dev/null +++ b/examples/math.qlib @@ -0,0 +1,9 @@ +qlib [ + name: "math" + version: 1 + description: "Operaciones matematicas basicas" + members: [ + %math/add.qvi + %math/subtract.qvi + ] +] diff --git a/examples/math/add.qvi b/examples/math/add.qvi new file mode 100644 index 0000000..ceed253 --- /dev/null +++ b/examples/math/add.qvi @@ -0,0 +1,54 @@ +Red [title: "add"] + +qvi-diagram: [ + connector: [ + input [pin: 1 label: "A" id: 1] + input [pin: 2 label: "B" id: 2] + output [pin: 3 label: "Result" id: 4] + ] + front-panel: [ + control [id: 1 type: 'control name: "ctrl_1" label: [text: "A"] default: 0.0] + control [id: 2 type: 'control name: "ctrl_2" label: [text: "B"] default: 0.0] + indicator [id: 4 type: 'indicator name: "ind_1" label: [text: "Result"]] + ] + block-diagram: [ + nodes: [ + node [id: 1 type: 'control x: 40 y: 80 name: "ctrl_1" label: [text: "A" visible: true]] + node [id: 2 type: 'control x: 40 y: 160 name: "ctrl_2" label: [text: "B" visible: true]] + node [id: 3 type: 'add x: 200 y: 120 name: "add_1" label: [text: "Add"]] + node [id: 4 type: 'indicator x: 360 y: 120 name: "ind_1" label: [text: "Result" visible: true]] + ] + wires: [ + wire [from: 1 port: 'out to: 3 port: 'a] + wire [from: 2 port: 'out to: 3 port: 'b] + wire [from: 3 port: 'out to: 4 port: 'in] + ] + ] +] + +arr-subset-helper: func [arr st ln] [copy/part skip arr to-integer st to-integer ln] + +add: context [ + exec: func [A B /local ctrl_1 ctrl_2 add_1 ind_1] [ + ctrl_1: A + ctrl_2: B + add_1: ctrl_1 + ctrl_2 + ind_1: add_1 + ind_1 + ] +] + +if not value? 'qtorres-runtime [ + view layout [ + text "A" f_1: field "0.0" + text "B" f_2: field "0.0" + button "Run" [ + ctrl_1: to-float f_1/text + ctrl_2: to-float f_2/text + add_1: ctrl_1 + ctrl_2 + ind_1: add_1 + l_ind_1/text: form ind_1 + ] + text "Result:" l_ind_1: text "---" + ] +] diff --git a/examples/math/subtract.qvi b/examples/math/subtract.qvi new file mode 100644 index 0000000..86d0823 --- /dev/null +++ b/examples/math/subtract.qvi @@ -0,0 +1,54 @@ +Red [title: "subtract"] + +qvi-diagram: [ + connector: [ + input [pin: 1 label: "A" id: 1] + input [pin: 2 label: "B" id: 2] + output [pin: 3 label: "Result" id: 4] + ] + front-panel: [ + control [id: 1 type: 'control name: "ctrl_1" label: [text: "A"] default: 0.0] + control [id: 2 type: 'control name: "ctrl_2" label: [text: "B"] default: 0.0] + indicator [id: 4 type: 'indicator name: "ind_1" label: [text: "Result"]] + ] + block-diagram: [ + nodes: [ + node [id: 1 type: 'control x: 40 y: 80 name: "ctrl_1" label: [text: "A" visible: true]] + node [id: 2 type: 'control x: 40 y: 160 name: "ctrl_2" label: [text: "B" visible: true]] + node [id: 3 type: 'sub x: 200 y: 120 name: "sub_1" label: [text: "Sub"]] + node [id: 4 type: 'indicator x: 360 y: 120 name: "ind_1" label: [text: "Result" visible: true]] + ] + wires: [ + wire [from: 1 port: 'out to: 3 port: 'a] + wire [from: 2 port: 'out to: 3 port: 'b] + wire [from: 3 port: 'out to: 4 port: 'in] + ] + ] +] + +arr-subset-helper: func [arr st ln] [copy/part skip arr to-integer st to-integer ln] + +subtract: context [ + exec: func [A B /local ctrl_1 ctrl_2 sub_1 ind_1] [ + ctrl_1: A + ctrl_2: B + sub_1: ctrl_1 - ctrl_2 + ind_1: sub_1 + ind_1 + ] +] + +if not value? 'qtorres-runtime [ + view layout [ + text "A" f_1: field "0.0" + text "B" f_2: field "0.0" + button "Run" [ + ctrl_1: to-float f_1/text + ctrl_2: to-float f_2/text + sub_1: ctrl_1 - ctrl_2 + ind_1: sub_1 + l_ind_1/text: form ind_1 + ] + text "Result:" l_ind_1: text "---" + ] +] diff --git a/examples/usa-libreria.qvi b/examples/usa-libreria.qvi new file mode 100644 index 0000000..e1f6e2f --- /dev/null +++ b/examples/usa-libreria.qvi @@ -0,0 +1,75 @@ +Red [title: "Usa libreria math"] + +qvi-diagram: [ + front-panel: [ + control [id: 1 type: 'control name: "ctrl_1" label: [text: "X"] default: 10.0] + control [id: 2 type: 'control name: "ctrl_2" label: [text: "Y"] default: 4.0] + indicator [id: 3 type: 'indicator name: "ind_1" label: [text: "Suma"]] + indicator [id: 4 type: 'indicator name: "ind_2" label: [text: "Resta"]] + ] + block-diagram: [ + nodes: [ + node [id: 1 type: 'control x: 40 y: 80 name: "ctrl_1" label: [text: "X" visible: true]] + node [id: 2 type: 'control x: 40 y: 180 name: "ctrl_2" label: [text: "Y" visible: true]] + node [id: 10 type: 'subvi x: 220 y: 80 name: "subvi_1" + file: %math/add.qvi + label: [text: "math/add"]] + node [id: 11 type: 'subvi x: 220 y: 180 name: "subvi_2" + file: %math/subtract.qvi + label: [text: "math/subtract"]] + node [id: 3 type: 'indicator x: 400 y: 80 name: "ind_1" label: [text: "Suma" visible: true]] + node [id: 4 type: 'indicator x: 400 y: 180 name: "ind_2" label: [text: "Resta" visible: true]] + ] + wires: [ + wire [from: 1 port: 'out to: 10 port: 'p1] + wire [from: 2 port: 'out to: 10 port: 'p2] + wire [from: 10 port: 'p3 to: 3 port: 'in] + wire [from: 1 port: 'out to: 11 port: 'p1] + wire [from: 2 port: 'out to: 11 port: 'p2] + wire [from: 11 port: 'p3 to: 4 port: 'in] + ] + ] +] + +arr-subset-helper: func [arr st ln] [copy/part skip arr to-integer st to-integer ln] + +_saved-qtorres-runtime: value? 'qtorres-runtime +qtorres-runtime: true +#include %math/add.qvi +#include %math/subtract.qvi +if not _saved-qtorres-runtime [unset 'qtorres-runtime] + +either empty? system/options/args [ + view layout [ + text "X" f_1: field "10.0" + text "Y" f_2: field "4.0" + button "Run" [ + ctrl_1: to-float f_1/text + ctrl_2: to-float f_2/text + subvi_1_p1: ctrl_1 + subvi_1_p2: ctrl_2 + subvi_1_p3: add/exec subvi_1_p1 subvi_1_p2 + ind_1: subvi_1_p3 + l_ind_1/text: form ind_1 + subvi_2_p1: ctrl_1 + subvi_2_p2: ctrl_2 + subvi_2_p3: subtract/exec subvi_2_p1 subvi_2_p2 + ind_2: subvi_2_p3 + l_ind_2/text: form ind_2 + ] + text "Suma:" l_ind_1: text "---" + text "Resta:" l_ind_2: text "---" + ] +][ + ctrl_1: to-float any [system/options/args/1 10.0] + ctrl_2: to-float any [system/options/args/2 4.0] + subvi_1_p1: ctrl_1 + subvi_1_p2: ctrl_2 + subvi_1_p3: add/exec subvi_1_p1 subvi_1_p2 + ind_1: subvi_1_p3 + subvi_2_p1: ctrl_1 + subvi_2_p2: ctrl_2 + subvi_2_p3: subtract/exec subvi_2_p1 subvi_2_p2 + ind_2: subvi_2_p3 + print rejoin ["Suma: " ind_1 " Resta: " ind_2] +] diff --git a/findings.md b/findings.md deleted file mode 100644 index 2978e0d..0000000 --- a/findings.md +++ /dev/null @@ -1,95 +0,0 @@ -# Findings — Fase 3: Sub-VI (#17) - -## Investigacion del codebase (2026-04-09) - -### Gaps identificados para Sub-VI - -| Componente | Estado actual | Que falta | -|------------|--------------|-----------| -| **blocks.red** | Sin entry para `'subvi` | Registro con puertos dinamicos (leidos del connector del .qvi cargado) | -| **model.red:make-node** | Sin campo `file` | Anadir campo `file: none` para nodos subvi | -| **model.red:make-diagram** | Campo `connector: none` existe (l.344) pero nunca se puebla | Parsear y poblar al cargar | -| **file-io.red:serialize-nodes** | No serializa `file:` | Anadir caso para nodos con `file` | -| **file-io.red:load-node-list** | Ignora `file:` del spec | Leer y almacenar en nodo | -| **file-io.red:load-vi** | No parsea `connector:` del qvi-diagram | Anadir regla parse | -| **file-io.red:serialize-diagram** | No emite `connector:` | Anadir seccion | -| **compiler.red:compile-body** | catch-all salta nodos sin bdef | Caso explicito `'subvi`: generar `do %file` + llamada a func | -| **compiler.red:compile-diagram** | Idem en run-body UI | Caso `'subvi` para UI | -| **canvas-render.red:in/out-ports** | Devuelven `[]` para subvi (no hay bdef) | Puertos dinamicos desde connector cargado | -| **canvas-render.red** | Label "SUBVI" ya existe (l.308) | OK, pero renderizar icono del sub-VI | -| **canvas-dialogs.red:open-palette** | Sin boton Sub-VI | Anadir boton + file picker | - -### Formato del connector (de suma-subvi.qvi) - -```red -connector: [ - input [id: 1 name: "ctrl_1" label: [text: "A"]] - input [id: 2 name: "ctrl_2" label: [text: "B"]] - output [id: 4 name: "ind_1" label: [text: "Resultado"]] -] -``` - -### Formato del nodo subvi en el caller (programa-con-subvi.qvi) - -```red -node [id: 10 type: 'subvi x: 200 y: 120 name: "subvi_1" file: %suma-subvi.qvi label: [text: "suma"]] -``` - -Puertos del nodo = labels del connector: `'A`, `'B`, `'Resultado`. - -### Codigo generado esperado (hand-written en ejemplo) - -**Sub-VI (suma-subvi.qvi):** -```red -suma: func [A [float!] B [float!]] [ - Resultado: A + B - Resultado -] -``` -- Nombre funcion = titulo del VI (Red [title: "suma"]) -- Parametros = label/text de connector inputs -- Retorno = ultima variable de connector outputs - -**Caller (programa-con-subvi.qvi):** -```red -do %suma-subvi.qvi ; carga la funcion -ind_1: suma ctrl_1 ctrl_2 ; llamada -``` - -### Decisiones tecnicas relevantes - -- **DT-006:** Sub-VIs generan `func` Red. Standalone con `if not value? 'qtorres-runtime` -- **DT-009:** VIs principales generan Red/View. Sub-VIs generan func sin UI. -- **DT-017:** Tipo de VI lo determina el contexto, no el VI. `connector` habilita uso como sub-VI. -- **DT-028:** Codigo generado debe compilar con `red -c`. Usar `#include` (compile-time), NO `do` (runtime). -- **DT-029 nivel 1:** try/catch por nodo en sub-VIs (Fase 3). - -### Decisiones tomadas en sesion de diseno (2026-04-09/10) - -1. **`#include` + `context`** — Cada sub-VI se envuelve en `context` con nombre (namespace). El caller usa `#include %subvi.qvi` (compile-time, cumple DT-028). Validado experimentalmente con 3 niveles de anidamiento. -2. **Convencion de llamada: `nombre/exec`** — Sub-VI genera `suma: context [exec: func [...] [...]]`. Caller llama `suma/exec arg1 arg2`. El context da namespace natural, sin colisiones. -3. **Standalone guard con save/restore** — El patron `_qt-imported: value? 'qtorres-runtime` + `if not _qt-imported [unset 'qtorres-runtime]` permite que cada VI funcione standalone Y como sub-VI. Validado con tests. -4. **Unicidad de nombres por titulo** — Nombre del context = titulo del VI. Compilador valida duplicados y da error. -5. **Sin deuda tecnica** — El context es extensible (se puede anadir `panel` func en el futuro). El runner sigue usando `do` en memoria para experiencia IDE completa. - -### Puntos de atencion - -1. **Puertos dinamicos** — A diferencia de otros bloques (puertos fijos en blocks.red), un subvi tiene puertos definidos por su connector. `in-ports`/`out-ports` en canvas-render.red deben leer del nodo, no del registry. -2. **Carga lazy del connector** — Al anadir un subvi al diagrama, hay que cargar el .qvi referenciado para leer su connector y extraer puertos. Si el fichero no existe, error amigable. -3. **port-var para subvi** — El compilador usa `node/name + "_" + port-name`. Para subvi los port names vienen del connector (ej: `subvi_1_A`). -4. **Nombre del context** — Viene del `title` del .qvi cargado. Se almacena en `node/config` como `[func-name "suma"]`. -5. **Multiples subvi del mismo .qvi** — Cada instancia es un nodo distinto con nombre unico, pero el `#include` se emite una sola vez. -6. **Round-trip** — serialize debe emitir `file:` en nodos subvi y `connector:` en el diagrama. - -### Test experimental: #include + context (2026-04-10) - -Verificado en `/tmp/red-include-test/` con `red-cli`: - -| Test | Resultado | -|------|-----------| -| `#include` de fichero con header `Red [...]` | Header del incluido se ignora ✓ | -| Context con nombre en fichero incluido | Accesible desde caller (`suma/exec`) ✓ | -| 3 niveles anidados (base → middle → top) | Todo funciona ✓ | -| `qvi-diagram` del caller no sobreescrito | Definir despues de includes → ultima asignacion gana ✓ | -| Standalone guard con `qtorres-runtime` | Sub-VIs no ejecutan standalone cuando son incluidos ✓ | -| Save/restore flag para VIs intermedios | `_qt-imported` + `unset 'qtorres-runtime` ✓ | diff --git a/progress.md b/progress.md deleted file mode 100644 index 622adfa..0000000 --- a/progress.md +++ /dev/null @@ -1,60 +0,0 @@ -# Progress Log — Fase 3: Sub-VI (#17) - -## Session 2026-04-09 — Planificacion - -### Cierre Fase 2 completado -- PR #62 mergeado: refactor 4D/4E, fixes cluster, v0.2.0 -- Ramas limpiadas: solo main queda (local y remoto) -- Tag v0.2.0 publicado -- Issues #28 y #49 movidos a fase-3 -- 462 tests PASS, linea base limpia - -### Investigacion Sub-VI -- Analisis exhaustivo del codebase: compiler, model, file-io, canvas, blocks -- Gaps documentados en findings.md -- Ejemplos existentes (suma-subvi.qvi, programa-con-subvi.qvi) son hand-written, no funcionales con el compilador actual -- Plan de 5 fases creado en task_plan.md -- Decisiones de diseno D1-D6 documentadas - -### Fase 1 — Modelo y serializacion COMPLETADA -- 1.1: Campo `file: none` añadido al prototipo de `make-node` -- 1.2: Helper `load-subvi-connector` implementado (carga connector desde .qvi) -- 1.3: Helper `make-subvi-node` implementado (crea nodo con file + config) -- 1.4: `serialize-nodes` emite `file:` para nodos subvi -- 1.5: `make-node` lee campo `file` del spec (carga) -- 1.6: `serialize-diagram` emite sección `connector:` -- 1.7: `load-vi` parsea `connector:` del qvi-diagram -- 462 tests PASS - -### Fase 2 — Compilador (parcial) -- 2.1: Bloque 'subvi registrado en blocks.red (category: 'function) -- 2.2: Función `compile-subvi-call` implementada -- 2.3: Caso 'subvi añadido a `compile-body` -- 2.4: Caso 'subvi añadido a `compile-diagram` (modo UI) -- 2.5-2.7: Pendientes (#include, func generation, unicidad) -- 462 tests PASS - -### Session 2026-04-10 — Revision de arquitectura (con Opus) - -**Cambio fundamental:** De inlining de funcs a `#include` + `context`. - -**Decisiones revisadas:** -- D4: `#include %subvi.qvi` en vez de inlinar funcs (validado con tests en /tmp/red-include-test/) -- D5: Sub-VI genera `nombre: context [exec: func [...] [...]]`, caller llama `nombre/exec` -- Patron save/restore de `qtorres-runtime` para VIs intermedios que incluyen sub-VIs -- Verificado: Red strip header de ficheros incluidos, qvi-diagram del caller no se sobreescribe - -**Problemas resueltos:** -- `do` rompe `red -c` → `#include` es compile-time ✓ -- `#include` de .qvi entero causa header duplicado → Red lo maneja ✓ -- Sub-VIs anidados → save/restore de flag funciona ✓ -- Colision de nombres → context da namespace natural ✓ - -**Impacto en Fase 2 (compilador):** -- 2.5: cambiar de inlining a emitir `#include` + save/restore -- 2.6: generar `context [exec: func [...]]` en vez de func bare -- 2.8: llamadas usan `nombre/exec` en vez de `nombre` -- Simplifica el compilador (no necesita compilar recursivamente sub-VIs) - -### Proximo paso -- Completar Fase 2: #include emission, context generation, validacion de unicidad diff --git a/src/graph/model.red b/src/graph/model.red index af85068..dc8fbaa 100644 --- a/src/graph/model.red +++ b/src/graph/model.red @@ -561,6 +561,10 @@ make-diagram-model: func [] [ broken-wire: none canvas-ref: none size: 0x0 + scroll-x: 0 ; BD scroll horizontal (píxeles de contenido) + scroll-y: 0 ; BD scroll vertical + fp-scroll-x: 0 ; FP scroll horizontal + fp-scroll-y: 0 ; FP scroll vertical ] ] diff --git a/src/io/file-io.red b/src/io/file-io.red index 3a252a5..c449659 100644 --- a/src/io/file-io.red +++ b/src/io/file-io.red @@ -846,4 +846,94 @@ save-panel-to-diagram: func [front-panel-items /local items item kw spec] [ reduce [to-set-word 'front-panel items] ] +; ══════════════════════════════════════════════════════════ +; QLIB — Librería de VIs con namespacing +; ══════════════════════════════════════════════════════════ +; +; Una .qlib es un FICHERO de texto con extension .qlib que actua como +; manifiesto. Los .qvi miembros viven junto a el (misma carpeta o subdir). +; +; Formato del fichero .qlib: +; qlib [ +; name: "math" +; version: 1 +; description: "Operaciones matematicas" +; members: [%math/add.qvi %math/subtract.qvi] +; ] +; +; Estructura tipica: +; proyecto/ +; math.qlib <- manifiesto +; math/ +; add.qvi +; subtract.qvi + +; Carga un fichero .qlib y devuelve un objeto con: +; name, version, description, dir, members (bloque de file! absolutos) +; Devuelve none si el fichero no es un .qlib valido. +load-qlib: func [ + "Carga el manifiesto de un fichero .qlib" + qlib-file [file!] + /local base-dir raw qd name version desc members-raw members m abs-path +][ + if dir? qlib-file [return none] + if not exists? qlib-file [return none] + raw: attempt [load qlib-file] + if not block? raw [return none] + if any [empty? raw raw/1 <> 'qlib] [return none] + qd: raw/2 + if not block? qd [return none] + + ; Directorio base = directorio que contiene el .qlib + base-dir: first split-path qlib-file + + name: any [select qd 'name ""] + version: any [select qd 'version 1] + desc: any [select qd 'description ""] + members-raw: any [select qd 'members copy []] + + ; Resolver rutas de miembros relativas al directorio del .qlib + members: copy [] + foreach m members-raw [ + if file? m [ + abs-path: to-file rejoin [form base-dir form m] + if exists? abs-path [append members abs-path] + ] + ] + + make object! compose/only [ + name: (name) + version: (version) + description: (desc) + dir: (base-dir) + members: (members) + ] +] + +; Busca ficheros .qlib en el directorio dado. +; Uso: find-qlibs/from system/options/path +; Devuelve bloque de objetos qlib (puede estar vacio). +find-qlibs: func [ + "Busca ficheros .qlib en el directorio dado" + /from project-dir [file!] + /local search-dirs libs d qlib-file obj +][ + search-dirs: copy [] + if from [append search-dirs clean-path project-dir] + + libs: copy [] + foreach p search-dirs [ + if all [p exists? p dir? p] [ + foreach d read p [ + if all [not dir? d %.qlib = suffix? d] [ + qlib-file: to-file rejoin [form p form d] + obj: load-qlib qlib-file + if obj [append libs obj] + ] + ] + ] + ] + libs +] + #include %../ui/diagram/canvas.red diff --git a/src/qtorres.red b/src/qtorres.red index e2326ea..a2056c4 100644 --- a/src/qtorres.red +++ b/src/qtorres.red @@ -6,6 +6,12 @@ Red [ Needs: 'View ] +; ── Directorio raíz del proyecto ───────────────────────────────── +; Red cambia what-dir al directorio del script al cargarlo (src/). +; Capturamos la raíz del proyecto (un nivel arriba de src/) antes +; de cualquier #include para que la paleta encuentre los .qlib. +_qtorres-project-dir: clean-path to-file rejoin [form what-dir "../"] + ; ── Módulos internos — se empaquetan con redc -e ───────────────── ; Chain loading (DT-025): cada módulo incluye al siguiente al final. ; Paths relativos al propio módulo → funciona con red-cli y redc -e. @@ -36,6 +42,15 @@ show-save-dialog: func [model [object!] /local dlg default-name] [ across button "Guardar" [ save-vi-full to-file _save-field/text _save-model + ; Actualizar títulos tras guardar con nuevo nombre + if all [fp-window object? fp-window] [ + fp-window/text: rejoin ["Front Panel — " _save-model/name] + show fp-window + ] + if all [bd-window object? bd-window] [ + bd-window/text: rejoin ["Block Diagram — " _save-model/name] + show bd-window + ] unview ] button "Cancelar" [unview] @@ -51,6 +66,7 @@ app-model: make app-model [ canvas-ref: none ; face del BD — para refrescar desde panel.red panel-ref: none ; face del FP — para refrescar desde canvas.red drag-is-label: false + current-file: none ; path del .qvi cargado — para buscar .qlib relativos ] ; ── save-vi-full: serializa BD + FP juntos ─────────────────────── @@ -225,6 +241,7 @@ btn-load: make face! [ app-model/structures: either in loaded 'structures [loaded/structures] [copy []] app-model/name: loaded/name app-model/front-panel: loaded/front-panel + app-model/current-file: path app-model/selected-node: none app-model/selected-wire: none app-model/selected-struct: none @@ -233,45 +250,115 @@ btn-load: make face! [ show canvas-face panel-face/draw: render-fp-panel app-model panel-face/size/x panel-face/size/y show panel-face + ; Actualizar títulos de ventanas + if all [fp-window object? fp-window] [ + fp-window/text: rejoin ["Front Panel — " app-model/name] + show fp-window + ] + if all [bd-window object? bd-window] [ + bd-window/text: rejoin ["Block Diagram — " app-model/name] + show bd-window + ] ] ] ] ] ] +; ── Tamaño fijo de ventanas ─────────────────────────────────────── +; Ambas ventanas tienen el mismo tamaño fijo — sin botón de maximizar +; ni redimensionado. El scroll maneja el contenido que supere el área. +; BD: canvas offset 5x38 (toolbar arriba) → canvas 890x557 +; FP: panel offset 5x5 → panel 890x590 +_win-size: 900x600 +_bd-canvas-sz: 890x557 ; _win-size - margen 10x43 (5+5 x 38+5) +_fp-panel-sz: 890x590 ; _win-size - margen 10x10 + ; ── Faces principales ───────────────────────────────────────────── -canvas-face: render-diagram app-model 880 490 +canvas-face: render-diagram app-model _bd-canvas-sz/x _bd-canvas-sz/y canvas-face/offset: 5x38 app-model/canvas-ref: canvas-face -panel-face: render-panel app-model 380 350 +panel-face: render-panel app-model _fp-panel-sz/x _fp-panel-sz/y panel-face/offset: 5x5 app-model/panel-ref: panel-face -; ── Ventana Front Panel (no-wait — coexiste con BD) ────────────── -view/no-wait make face! [ +; ── Referencias globales a las ventanas ────────────────────────── +; Necesarias para toggle BD (Ctrl+E) y actualizar títulos al cargar .qvi. +bd-window: none +fp-window: none + +; ── show-bd-window: abre BD o intenta traerlo al frente ────────── +; GTK-013: Red/View no expone gtk_window_present — `show` intenta +; elevar pero GTK no lo garantiza. La recreación (cuando bd-window +; es none tras cerrar con X) sí garantiza que aparezca al frente. +show-bd-window: func [/local] [ + ; Ya existe → intentar traer al frente con show + if all [bd-window object? bd-window] [ + show bd-window + exit + ] + ; Crear ventana BD (primera vez o tras cierre con X → siempre al frente) + bd-window: make face! [ + type: 'window + text: rejoin ["Block Diagram — " app-model/name] + size: _win-size + offset: 60x60 + pane: reduce [btn-run btn-save btn-load canvas-face] + actors: make object! [ + on-key-down: func [face event] [ + ; Delete/Backspace → borrar selección en canvas + if any [ + find [delete backspace] event/key + find [#"^(7F)" #"^H"] event/key + ][ + canvas-delete-selected canvas-face + ] + ; Ctrl+E desde BD → intentar traer FP al frente + if event/ctrl? [ + if any [event/key = #"e" event/key = #"E" event/key = #"^E"] [ + if all [fp-window object? fp-window] [ + show fp-window + ] + ] + ] + ] + on-close: func [face event] [ + ; GTK destruye la ventana al cerrar — limpiamos referencia. + ; El próximo Ctrl+E recreará la ventana al frente. + bd-window: none + ] + ] + ] + view/no-wait bd-window +] + +; ── Ventana Block Diagram (esclavo — se abre con Ctrl+E) ───────── +show-bd-window + +; ── Ventana Front Panel (maestra — mantiene el event loop) ─────── +; Cerrar FP = cerrar todo. Ctrl+E = toggle BD. +fp-window: make face! [ type: 'window text: "Front Panel — untitled" - size: 400x375 + size: _win-size offset: 960x60 pane: reduce [panel-face] -] - -; ── Ventana Block Diagram (blocking — mantiene el event loop) ──── -view make face! [ - type: 'window - text: "Block Diagram — untitled" - size: 900x545 - offset: 60x60 - pane: reduce [btn-run btn-save btn-load canvas-face] actors: make object! [ - on-key: func [face event] [ - if any [ - find [delete backspace] event/key - find [#"^(7F)" #"^H"] event/key - ][ - canvas-delete-selected canvas-face + on-key-down: func [face event] [ + ; Ctrl+E → mostrar BD (crearlo si se cerró, traerlo al frente si existe) + ; GTK-012: on-key-down para combos Ctrl; view/no-wait levanta la ventana en GTK + ; Nota: Ctrl+E nunca cierra BD — para cerrar BD usar la X del OS + if event/ctrl? [ + if any [event/key = #"e" event/key = #"E" event/key = #"^E"] [ + show-bd-window + ] ] ] + on-close: func [face event] [ + ; Cerrar FP = cerrar todo el entorno + unview/all + ] ] ] +view fp-window diff --git a/src/ui/diagram/canvas-dialogs.red b/src/ui/diagram/canvas-dialogs.red index 5fbd7bb..ceec774 100644 --- a/src/ui/diagram/canvas-dialogs.red +++ b/src/ui/diagram/canvas-dialogs.red @@ -273,6 +273,35 @@ palette-add-node: func [node-type /local n nid model] [ unview ] +; Añade un nodo Sub-VI apuntando directamente a un .qvi de librería. +; vi-path es un file! ya resuelto (absoluto o relativo al working dir). +palette-add-qlib-vi: func [vi-path [file!] /local n nid model] [ + model: palette-canvas/extra + nid: gen-node-id model + n: make-subvi-node compose [ + id: (nid) + type: 'subvi + x: (palette-pos-x) + y: (palette-pos-y) + file: (vi-path) + ] + either palette-struct [ + if all [palette-struct/type = 'case-structure block? palette-struct/frames] [ + if palette-struct/active-frame < length? palette-struct/frames [ + append palette-struct/frames/(palette-struct/active-frame + 1)/nodes n + ] + ] + if find [while-loop for-loop] palette-struct/type [ + append palette-struct/nodes n + ] + ][ + append model/nodes n + ] + palette-canvas/draw: render-bd model + show palette-canvas + unview +] + ; Añade un nodo Sub-VI con file picker. palette-add-subvi: func [/local n nid model file-path] [ model: palette-canvas/extra @@ -319,12 +348,16 @@ palette-add-structure: func [type [word!] /local nid st model] [ unview ] -open-palette: func [face x y /struct target-struct] [ +open-palette: func [face x y /struct target-struct + /local qlibs qlib vi-path vi-label vi-short layout-block +][ palette-canvas: face palette-pos-x: x palette-pos-y: y palette-struct: target-struct - view/no-wait [ + + ; ── Parte estática ──────────────────────────────────────────── + layout-block: copy [ title "Añadir bloque" text "Aritmética:" return button 80 "Add +" [palette-add-node 'add] @@ -362,7 +395,7 @@ open-palette: func [face x y /struct target-struct] [ button 80 "While" [palette-add-structure 'while-loop] button 80 "For" [palette-add-structure 'for-loop] button 80 "Case" [palette-add-structure 'case-structure] - button 80 "QVI" [palette-add-subvi] return + button 80 "QVI" [palette-add-subvi] return button 80 "Add SR" [ if palette-struct [ unview @@ -370,8 +403,43 @@ open-palette: func [face x y /struct target-struct] [ ] ] return - button "Cancelar" [unview] ] + + ; ── Sección dinámica: librerías .qlib ──────────────────────── + ; Busca .qlib junto al .qvi cargado; si no hay fichero abierto, usa raíz del proyecto + _qlib-search-dir: either all [ + value? 'app-model + object? app-model + in app-model 'current-file + file? app-model/current-file + ][ + first split-path app-model/current-file + ][ + _qtorres-project-dir + ] + qlibs: find-qlibs/from _qlib-search-dir + if not empty? qlibs [ + append layout-block [text "Librerías:" return] + foreach qlib qlibs [ + foreach vi-path qlib/members [ + ; Etiqueta: "nombre-lib/vi" (sin extensión) + vi-short: form last split-path vi-path + if find vi-short ".qvi" [ + vi-short: copy/part vi-short (subtract length? vi-short 4) + ] + vi-label: rejoin [qlib/name "/" vi-short] + ; compose/deep captura vi-path por valor en cada iteración + append layout-block compose/deep [ + button 120 (vi-label) [palette-add-qlib-vi (vi-path)] + ] + ] + append layout-block 'return + ] + ] + + append layout-block [button "Cancelar" [unview]] + + view/no-wait layout-block ] ; ── Shift Register helpers ────────────────────────────────────────── diff --git a/src/ui/diagram/canvas-render.red b/src/ui/diagram/canvas-render.red index e18a654..ebb550d 100644 --- a/src/ui/diagram/canvas-render.red +++ b/src/ui/diagram/canvas-render.red @@ -852,11 +852,40 @@ render-structure: func [ cmds ] -render-bd: func [model /local cmds src-port-xy mid st] [ +; Bounding-box del contenido del BD en píxeles de contenido +bd-content-bounds: func [model /local cx cy] [ + ; Mínimo 0 — el caller añade max con el tamaño del viewport para + ; que solo aparezcan scrollbars cuando el contenido supera la ventana. + cx: 0 cy: 0 + foreach _n model/nodes [ + cx: max cx (_n/x + block-width + 40) + cy: max cy (_n/y + block-height + 40) + ] + if block? model/structures [ + foreach _st model/structures [ + cx: max cx (_st/x + _st/w + 40) + cy: max cy (_st/y + _st/h + 40) + ] + ] + as-pair cx cy +] + +render-bd: func [model /local cmds src-port-xy mid st w h sx sy sb-w _cx _cy _th _tx _ty _bounds] [ cmds: copy [] - ; 0) Grid de fondo - append cmds render-grid 880 490 + ; Dimensiones del canvas (dinámicas con resize) + w: either all [object? model/canvas-ref pair? model/canvas-ref/size] + [model/canvas-ref/size/x] [880] + h: either all [object? model/canvas-ref pair? model/canvas-ref/size] + [model/canvas-ref/size/y] [490] + sx: any [model/scroll-x 0] + sy: any [model/scroll-y 0] + + ; Translate por scroll — Red/View clipea automáticamente a los bounds del face + append cmds compose [translate (as-pair (negate sx) (negate sy))] + + ; 0) Grid de fondo (cubre el área visible en espacio de contenido) + append cmds render-grid (sx + w + grid-size) (sy + h + grid-size) ; 1) Estructuras contenedoras (detrás de los nodos normales) if block? model/structures [ @@ -985,5 +1014,37 @@ render-bd: func [model /local cmds src-port-xy mid st] [ ; 5) Nodos normales (encima de las estructuras) append cmds render-node-list model/nodes model/selected-node + ; ── Fin del espacio de contenido (volver a coords de pantalla) ── + append cmds [reset-matrix] + + ; ── Scrollbars (coords de pantalla — fuera del translate) ─────── + ; Contenido real vs viewport: scrollbar solo si contenido > viewport. + _bounds: bd-content-bounds model + _cx: max w _bounds/x + _cy: max h _bounds/y + sb-w: 8 ; grosor del scrollbar + ; Scrollbar vertical (derecha) + if _cy > h [ + _th: max 20 to-integer (h * h / _cy) + _ty: to-integer (sy * (h - _th - sb-w) / (_cy - h)) + append cmds compose [ + fill-pen 210.212.218 pen off + box (as-pair (w - sb-w) 0) (as-pair w (h - sb-w)) + fill-pen 150.152.162 pen off + box (as-pair (w - sb-w) (_ty)) (as-pair w (_ty + _th)) + ] + ] + ; Scrollbar horizontal (abajo) + if _cx > w [ + _th: max 20 to-integer (w * w / _cx) + _tx: to-integer (sx * (w - _th - sb-w) / (_cx - w)) + append cmds compose [ + fill-pen 210.212.218 pen off + box (as-pair 0 (h - sb-w)) (as-pair (w - sb-w) h) + fill-pen 150.152.162 pen off + box (as-pair (_tx) (h - sb-w)) (as-pair (_tx + _th) h) + ] + ] + cmds ] diff --git a/src/ui/diagram/canvas.red b/src/ui/diagram/canvas.red index 80db292..9975666 100644 --- a/src/ui/diagram/canvas.red +++ b/src/ui/diagram/canvas.red @@ -493,10 +493,28 @@ render-diagram: func [model canvas-width canvas-height /local canvas-face] [ extra: model ; modelo accesible desde actores via face/extra actors: make object! [ - on-down: func [face event /local mouse-x mouse-y model hit-result hit-nd hit-port-name hit-dir hit-ref] [ + on-down: func [face event /local mouse-x mouse-y model hit-result hit-nd hit-port-name hit-dir hit-ref _sx _sy _w _h _b _sb] [ model: face/extra - mouse-x: event/offset/x - mouse-y: event/offset/y + _sx: event/offset/x _sy: event/offset/y + _w: face/size/x _h: face/size/y + _sb: 8 + ; ── Click en scrollbar (coords de pantalla, antes del translate) ── + _b: bd-content-bounds model + if all [_b/y > _h _sx >= (_w - _sb) _sy < (_h - _sb)] [ + ; Scrollbar vertical — calcular nueva posición de scroll + model/scroll-y: max 0 to-integer (_sy * (_b/y - _h) / (_h - _sb)) + face/draw: render-bd model + exit + ] + if all [_b/x > _w _sy >= (_h - _sb) _sx < (_w - _sb)] [ + ; Scrollbar horizontal + model/scroll-x: max 0 to-integer (_sx * (_b/x - _w) / (_w - _sb)) + face/draw: render-bd model + exit + ] + ; ── Hit-test normal (coords de contenido, con compensación de scroll) ── + mouse-x: event/offset/x + model/scroll-x + mouse-y: event/offset/y + model/scroll-y ; 1) Puerto? (incluye nodos internos de estructuras) hit-result: hit-port model mouse-x mouse-y @@ -509,7 +527,7 @@ render-diagram: func [model canvas-width canvas-height /local canvas-face] [ model/broken-wire: none model/wire-src: hit-nd model/wire-port: hit-port-name - model/mouse-pos: event/offset + model/mouse-pos: as-pair mouse-x mouse-y face/draw: render-bd model ] ][ @@ -921,8 +939,8 @@ render-diagram: func [model canvas-width canvas-height /local canvas-face] [ on-over: func [face event /local mouse-x mouse-y model dx dy _st _frame _nodes] [ model: face/extra - mouse-x: event/offset/x - mouse-y: event/offset/y + mouse-x: event/offset/x + model/scroll-x + mouse-y: event/offset/y + model/scroll-y ; Drag de nodo (normal o interno) if all [model/drag-node model/drag-off event/down?] [ @@ -992,7 +1010,7 @@ render-diagram: func [model canvas-width canvas-height /local canvas-face] [ model: face/extra ; Completar wire si se suelta sobre un puerto de entrada (drag-to-connect) if model/wire-src [ - hit-result: hit-port model event/offset/x event/offset/y + hit-result: hit-port model (event/offset/x + model/scroll-x) (event/offset/y + model/scroll-y) if all [ hit-result hit-result/3 = 'in @@ -1051,10 +1069,24 @@ render-diagram: func [model canvas-width canvas-height /local canvas-face] [ ] ] + on-wheel: func [face event /local model step bounds max-sx max-sy] [ + model: face/extra + step: to-integer event/picked * -40 + bounds: bd-content-bounds model + max-sx: max 0 (bounds/x - face/size/x) + max-sy: max 0 (bounds/y - face/size/y) + either event/shift? [ + model/scroll-x: max 0 min max-sx (model/scroll-x + step) + ][ + model/scroll-y: max 0 min max-sy (model/scroll-y + step) + ] + face/draw: render-bd model + ] + on-dbl-click: func [face event /local mouse-x mouse-y model node label-text st-hit struct-hit sr-hit] [ model: face/extra - mouse-x: event/offset/x - mouse-y: event/offset/y + mouse-x: event/offset/x + model/scroll-x + mouse-y: event/offset/y + model/scroll-y ; 0) Terminal SR: editar valor inicial sr-hit: hit-structure-sr model mouse-x mouse-y diff --git a/src/ui/panel/panel-render.red b/src/ui/panel/panel-render.red index 79a74c1..29e5845 100644 --- a/src/ui/panel/panel-render.red +++ b/src/ui/panel/panel-render.red @@ -396,16 +396,62 @@ render-fp-item: func [item selected? /local cmds col border-col type-lbl led-col cmds ] -render-fp-panel: func [model w h /local cmds item selected?] [ +fp-content-bounds: func [model /local cx cy] [ + ; Mínimo 0 — el caller añade max con el tamaño del viewport. + cx: 0 cy: 0 + foreach _item model/front-panel [ + cx: max cx (_item/offset/x + fp-item-width + 20) + cy: max cy (_item/offset/y + fp-item-height + fp-label-above + 20) + ] + as-pair cx cy +] + +render-fp-panel: func [model w h /local cmds item selected? sx sy sb-w _bounds _cx _cy _th _tx _ty] [ cmds: copy [] - append cmds render-fp-grid w h + sx: any [model/fp-scroll-x 0] + sy: any [model/fp-scroll-y 0] + + ; Viewport: translate por scroll — Red/View clipea automáticamente a los bounds del face + append cmds compose [translate (as-pair (negate sx) (negate sy))] + + append cmds render-fp-grid (sx + w + 20) (sy + h + 20) foreach item model/front-panel [ selected?: either model/selected-fp [same? item model/selected-fp] [false] append cmds render-fp-item item selected? ] + ; Volver a coords de pantalla para scrollbars + append cmds [reset-matrix] + + _bounds: fp-content-bounds model + _cx: max w _bounds/x + _cy: max h _bounds/y + sb-w: 8 + ; Scrollbar vertical (derecha) + if _cy > h [ + _th: max 20 to-integer (h * h / _cy) + _ty: to-integer (sy * (h - _th - sb-w) / (_cy - h)) + append cmds compose [ + fill-pen 210.212.218 pen off + box (as-pair (w - sb-w) 0) (as-pair w (h - sb-w)) + fill-pen 150.152.162 pen off + box (as-pair (w - sb-w) (_ty)) (as-pair w (_ty + _th)) + ] + ] + ; Scrollbar horizontal (abajo) + if _cx > w [ + _th: max 20 to-integer (w * w / _cx) + _tx: to-integer (sx * (w - _th - sb-w) / (_cx - w)) + append cmds compose [ + fill-pen 210.212.218 pen off + box (as-pair 0 (h - sb-w)) (as-pair (w - sb-w) h) + fill-pen 150.152.162 pen off + box (as-pair (_tx) (h - sb-w)) (as-pair (_tx + _th) h) + ] + ] + cmds ] diff --git a/src/ui/panel/panel.red b/src/ui/panel/panel.red index 4cd8fd7..685a47c 100644 --- a/src/ui/panel/panel.red +++ b/src/ui/panel/panel.red @@ -342,11 +342,26 @@ render-panel: func [model panel-width panel-height /local panel-face] [ draw: render-fp-panel model panel-width panel-height actors: make object! [ - on-down: func [face event /local mouse-x mouse-y zone item w h lbl-dx lbl-dy] [ - mouse-x: event/offset/x - mouse-y: event/offset/y - w: face/extra/size/x - h: face/extra/size/y + on-down: func [face event /local mouse-x mouse-y zone item w h lbl-dx lbl-dy _sx _sy _sb _bounds _cx _cy] [ + w: face/size/x h: face/size/y + _sx: event/offset/x _sy: event/offset/y + _sb: 8 + _bounds: fp-content-bounds face/extra + _cx: _bounds/x _cy: _bounds/y + ; ── Click en scrollbar vertical del FP ── + if all [_cy > h _sx >= (w - _sb) _sy < (h - _sb)] [ + face/extra/fp-scroll-y: max 0 min (max 0 (_cy - h)) to-integer (_sy * (_cy - h) / (h - _sb)) + face/draw: render-fp-panel face/extra w h + exit + ] + ; ── Click en scrollbar horizontal del FP ── + if all [_cx > w _sy >= (h - _sb) _sx < (w - _sb)] [ + face/extra/fp-scroll-x: max 0 min (max 0 (_cx - w)) to-integer (_sx * (_cx - w) / (w - _sb)) + face/draw: render-fp-panel face/extra w h + exit + ] + mouse-x: event/offset/x + face/extra/fp-scroll-x + mouse-y: event/offset/y + face/extra/fp-scroll-y zone: hit-fp-zone face/extra mouse-x mouse-y either zone [ @@ -376,10 +391,10 @@ render-panel: func [model panel-width panel-height /local panel-face] [ ] on-over: func [face event /local mouse-x mouse-y w h item] [ - mouse-x: event/offset/x - mouse-y: event/offset/y - w: face/extra/size/x - h: face/extra/size/y + mouse-x: event/offset/x + face/extra/fp-scroll-x + mouse-y: event/offset/y + face/extra/fp-scroll-y + w: face/size/x + h: face/size/y if all [face/extra/drag-fp face/extra/drag-off event/down?] [ item: face/extra/drag-fp @@ -403,10 +418,10 @@ render-panel: func [model panel-width panel-height /local panel-face] [ ] on-click: func [face event /local mouse-x mouse-y hit w h] [ - mouse-x: event/offset/x - mouse-y: event/offset/y - w: face/extra/size/x - h: face/extra/size/y + mouse-x: event/offset/x + face/extra/fp-scroll-x + mouse-y: event/offset/y + face/extra/fp-scroll-y + w: face/size/x + h: face/size/y hit: hit-fp-item face/extra mouse-x mouse-y case [ all [hit hit/type = 'bool-control] [ @@ -430,8 +445,8 @@ render-panel: func [model panel-width panel-height /local panel-face] [ ] on-dbl-click: func [face event /local mouse-x mouse-y hit] [ - mouse-x: event/offset/x - mouse-y: event/offset/y + mouse-x: event/offset/x + face/extra/fp-scroll-x + mouse-y: event/offset/y + face/extra/fp-scroll-y hit: hit-fp-item face/extra mouse-x mouse-y case [ @@ -448,16 +463,30 @@ render-panel: func [model panel-width panel-height /local panel-face] [ ] on-alt-down: func [face event /local mouse-x mouse-y] [ - mouse-x: event/offset/x - mouse-y: event/offset/y + mouse-x: event/offset/x + face/extra/fp-scroll-x + mouse-y: event/offset/y + face/extra/fp-scroll-y open-fp-palette face mouse-x mouse-y ] + on-wheel: func [face event /local model step bounds max-sx max-sy] [ + model: face/extra + step: to-integer event/picked * -40 + bounds: fp-content-bounds model + max-sx: max 0 (bounds/x - face/size/x) + max-sy: max 0 (bounds/y - face/size/y) + either event/shift? [ + model/fp-scroll-x: max 0 min max-sx (model/fp-scroll-x + step) + ][ + model/fp-scroll-y: max 0 min max-sy (model/fp-scroll-y + step) + ] + face/draw: render-fp-panel model face/size/x face/size/y + ] + on-key: func [face event /local model hit w h _cref bd-node] [ model: face/extra hit: model/selected-fp - w: model/size/x - h: model/size/y + w: face/size/x + h: face/size/y if all [hit any [find [delete backspace] event/key find [#"^(7F)" #"^H"] event/key]] [ ; Sync BD: borrar nodo y sus wires diff --git a/task_plan.md b/task_plan.md deleted file mode 100644 index 27f952e..0000000 --- a/task_plan.md +++ /dev/null @@ -1,224 +0,0 @@ -# Plan — Fase 3: Sub-VI con connector pane (#17) - -**Creado:** 2026-04-09 -**Objetivo:** Permitir que un VI con `connector` se use como bloque dentro de otro VI, con puertos dinamicos, compilacion a `func` Red, y round-trip completo. - -**Linea base:** 462 tests PASS, v0.2.0, main limpia. - -## Reglas absolutas (recordatorio) - -- Todo en Red-Lang. Sin crear modulos nuevos sin aprobacion. -- `./red-cli tests/run-all.red` debe pasar tras cada cambio. -- Consultar `skills/red-lang/SKILL.md` antes de tocar Draw/View. -- NUNCA `do` dinamico, `load` strings, ni `compose` runtime en .qvi generado (DT-028). -- Los puertos del subvi vienen del connector del .qvi cargado, no de blocks.red. - -## Decisiones de diseno - -### D1: Puertos dinamicos vs registro en blocks.red - -**Decision:** Los puertos del nodo subvi se almacenan en `node/config` como `[connector [...]]` al momento de insertar el nodo. `in-ports`/`out-ports` en canvas-render.red consultan esta config cuando `node/type = 'subvi`. blocks.red tiene un entry minimo (categoria, sin puertos fijos). - -**Razon:** Cada subvi tiene puertos distintos segun su connector. No se puede registrar en blocks.red con puertos fijos. - -### D2: Campo `file` en el nodo - -**Decision:** Anadir campo `file: none` al prototipo de nodo en `make-node`. Solo se puebla para nodos `'subvi`. Se serializa/carga en file-io.red. - -### D3: Nombre del context = titulo del VI (unicidad obligatoria) - -**Decision:** El nombre del context viene del `title` del header Red del .qvi cargado. Se almacena en `node/config` como `[func-name "suma"]`. El compilador valida que no haya dos sub-VIs con el mismo titulo — si colisionan, error de compilacion. - -**Razon:** Igual que LabVIEW, donde los nombres de VI deben ser unicos dentro del proyecto. El context da namespace natural (`suma/exec`). - -### D4: `#include` + context (validado con tests) - -**Decision:** El codigo generado usa `#include %subvi.qvi` con cada sub-VI envuelto en un `context` con nombre. Validado experimentalmente con 3 niveles de anidamiento. - -**Patron del sub-VI (.qvi con connector):** -```red -Red [title: "suma" Needs: 'View] -qvi-diagram: [...] - -suma: context [ - exec: func [A [float!] B [float!] /local Resultado] [ - Resultado: A + B - Resultado - ] -] - -if not value? 'qtorres-runtime [ - context [view layout [...]] -] -``` - -**Patron del sub-VI que usa otros sub-VIs:** -```red -Red [title: "filtro" Needs: 'View] -qvi-diagram: [...] - -_qt-imported: value? 'qtorres-runtime -qtorres-runtime: true -#include %suma.qvi -if not _qt-imported [unset 'qtorres-runtime] - -filtro: context [ - exec: func [X [float!]] [suma/exec X 0.5] -] - -if not value? 'qtorres-runtime [ - context [view layout [...]] -] -``` - -**Patron del VI caller (programa principal):** -```red -Red [title: "main" Needs: 'View] -qvi-diagram: [...] - -qtorres-runtime: true -#include %filtro.qvi - -context [ - view layout [ - button "Run" [l_ind_1/text: form filtro/exec to-float f_ctrl_1/text] - ] -] -``` - -**Comportamiento verificado:** -- `red suma.qvi` → standalone, muestra panel ✓ -- `red filtro.qvi` → standalone, muestra panel (suma NO ejecuta standalone) ✓ -- `red main.qvi` → solo main, ni filtro ni suma ejecutan standalone ✓ -- `red -c main.qvi` → compilable, #include es compile-time ✓ - -**Ventajas sobre inlining:** -- Cada VI es 100% independiente — ejecutable por si solo -- El compilador de QTorres solo emite #include + llamadas, NO necesita compilar recursivamente sub-VIs -- Los namespaces (context) evitan colisiones de forma natural -- El .qvi del sub-VI es la fuente de verdad — si cambia, el caller lo ve al recompilar - -**Razon:** `#include` es compile-time (cumple DT-028). El context con nombre da namespace. El patron save/restore de `qtorres-runtime` permite que cada VI funcione standalone Y como sub-VI sin conflictos. - -### D5: `context` con `exec` para sub-VIs - -**Decision:** El codigo generado sigue esta estructura: -- **VI con connector:** `nombre: context [exec: func [...] [...]]` + standalone guard -- **VI sin connector (solo standalone):** `context [view layout [...]]` -- **VI que usa sub-VIs:** save/restore flag + `#include`s + su propio context (si tiene connector) o standalone - -**Convencion de llamada:** `suma/exec arg1 arg2` — el context es el namespace, `exec` es la funcion. - -**Razon:** No hay diferencia entre "VI principal" y "sub-VI" — un VI con connector siempre genera context + standalone guard, independientemente de como se use (DT-017). El caller decide si lo incluye. - -### D6: Connector se edita manualmente (por ahora) - -**Decision:** Un VI que quiera ser usable como sub-VI necesita una seccion `connector:` en su `qvi-diagram`. En esta fase, el connector se edita manualmente en el .qvi. El editor visual de connector pane es fase posterior. - -### D7: Error handling (DT-029 nivel 1) - -**Decision:** Cada llamada a sub-VI se envuelve en `try`: `result: try [subvi-name/exec arg1 arg2]`. Si falla, se propaga el error nativo de Red. - -### D8: Vision a largo plazo — sin deuda tecnica - -El modelo de dos caminos (runner vs .qvi generado) permite: -- **Runner (IDE):** abrir multiples VIs, sub-VI mostrando su panel, valores en vivo — todo posible via `do` en memoria + `view/no-wait` -- **Compilado (.qvi):** binario autocontenido via `#include`. Sub-VIs son context con `exec` (sin panel) en Fase 3. - -**Evolucion futura sin romper arquitectura:** -- Sub-VIs con panel en compilado: anadir func `panel` al context → `suma/panel`. Cambio aditivo, no rompe `exec`. -- Multiples VIs abiertos: cada context es independiente, no comparten estado. -- Clases (.qclass): futuro, modelo diferente. -- El unico riesgo conocido es `app-model` unico (un VI en memoria), que se abordara con .qproj. - -Las decisiones de esta fase no bloquean ninguna de estas evoluciones. - -## Fases de implementacion - -### Fase 1 — Modelo y serializacion ✅ COMPLETADA - -> Cimientos: que el formato se cargue, persista y haga round-trip. - -- [x] **1.1** `model.red`: campo `file: none` en `make-node` -- [x] **1.2** `model.red`: helper `load-subvi-connector` -- [x] **1.3** `model.red`: helper `make-subvi-node` -- [x] **1.4** `file-io.red`: `serialize-nodes` emite `file:` -- [x] **1.5** `file-io.red`: `load-node-list` lee `file:` -- [x] **1.6** `file-io.red`: `serialize-diagram` emite `connector:` -- [x] **1.7** `file-io.red`: `load-vi` parsea `connector:` -- [x] **1.8** Tests round-trip -- [x] **1.9** 462 tests PASS - -### Fase 2 — Compilador ⬜ - -> Que el codigo generado sea correcto para caller y callee. - -- [x] **2.1** `blocks.red`: registrar `'subvi` con block-def minimo (category: 'function, sin puertos, sin emit) -- [x] **2.2** `compiler.red`: funcion `compile-subvi-call` que genera la llamada `nombre/exec arg1 arg2` -- [x] **2.3** `compiler.red`: en `compile-body`, caso `item/type = 'subvi` → `compile-subvi-call` -- [x] **2.4** `compiler.red`: en `compile-diagram` run-body, caso `'subvi` para modo UI -- [ ] **2.5** `compiler.red`: emitir `#include %subvi.qvi` + save/restore `qtorres-runtime` al inicio del codigo generado. Recopilar ficheros unicos (sin duplicados). -- [ ] **2.6** `compiler.red`: para VIs con connector propio, generar `nombre: context [exec: func [...] [...]]` + standalone guard con save/restore -- [ ] **2.7** `compiler.red`: validar unicidad de func-name entre todos los sub-VIs referenciados — error si colision -- [ ] **2.8** Actualizar `compile-subvi-call` para usar convencion `nombre/exec` en vez de func directa -- [ ] **2.9** Tests: compile-body con nodo subvi, codigo generado correcto, round-trip compile -- [ ] **2.10** Tests pasan. Commit. - -### Fase 3 — Renderizado y UI ⬜ - -> Que el subvi se vea y se pueda anadir desde el editor. - -- [ ] **3.1** `canvas-render.red`: `in-ports` / `out-ports` — si `node/type = 'subvi`, leer puertos de `node/config` en vez de blocks registry -- [ ] **3.2** `canvas-render.red`: renderizar nodo subvi con icono (si tiene) o caja generica con label = func-name -- [ ] **3.3** `canvas-render.red`: colores de puertos segun tipo del connector (number/string/boolean/etc) -- [ ] **3.4** `canvas-dialogs.red`: boton "Sub-VI" en paleta → file picker (`request-file`) → `make-subvi-node` → anadir al diagrama -- [ ] **3.5** `canvas.red`: hit-test de puertos del subvi (misma logica que otros nodos, pero puertos dinamicos) -- [ ] **3.6** Test manual: crear diagrama con subvi, conectar wires, verificar render -- [ ] **3.7** Commit. - -### Fase 4 — Ejemplo funcional end-to-end ⬜ - -> Que suma-subvi.qvi + programa-con-subvi.qvi funcionen de verdad. - -- [ ] **4.1** Actualizar `examples/suma-subvi.qvi`: qvi-diagram con connector + codigo generado (context + standalone guard) -- [ ] **4.2** Actualizar `examples/programa-con-subvi.qvi`: qvi-diagram con nodo subvi + codigo generado (#include + context) -- [ ] **4.3** Verificar: `./red-cli examples/programa-con-subvi.qvi` produce resultado correcto -- [ ] **4.4** Verificar: cargar en QTorres, editar, guardar, volver a cargar → round-trip OK -- [ ] **4.5** Test automatizado: headless round-trip del ejemplo -- [ ] **4.6** Commit + PR. - -### Fase 5 — Cierre ⬜ - -- [ ] **5.1** Actualizar CLAUDE.md (estado Fase 3, nuevos ficheros/funciones, D4 como nueva DT) -- [ ] **5.2** Cerrar Issue #17 -- [ ] **5.3** Actualizar version a 0.3.0 y tag - -## Criterios de exito - -- `./red-cli tests/run-all.red` → todos pasan -- `./red-cli examples/suma-subvi.qvi` → standalone funciona -- `./red-cli examples/programa-con-subvi.qvi` → output correcto (headless, usa sub-VI) -- Round-trip: cargar .qvi con subvi → guardar → cargar → mismos datos -- Un VI con connector genera `nombre: context [exec: func [...]]` + standalone guard -- Un VI caller genera `#include` + llamada `nombre/exec` correcta -- Compilador detecta colision de nombres entre sub-VIs -- Nodo subvi se renderiza con puertos del connector en el canvas - -## Riesgos - -| Riesgo | Mitigacion | -|--------|-----------| -| `#include` de .qvi con header Red | Verificado: Red strip header de ficheros incluidos ✓ | -| `qvi-diagram` sobreescrito por includes | Caller define su qvi-diagram DESPUES de includes → ultima asignacion gana ✓ | -| Standalone guard en sub-VIs anidados | Patron save/restore validado con 3 niveles ✓ | -| Connector con tipos no-number (string, bool, cluster) | Fase 1 solo number, extender despues | -| Fichero .qvi referenciado no existe | Error amigable en `load-subvi-connector`, no crash | -| Puertos dinamicos rompen hit-test en canvas | Reusar misma logica de port positioning pero con lista dinamica | -| Cambio en connector del subvi invalida el caller | Detectar en load, warning al usuario — fase posterior | -| Dos sub-VIs con mismo titulo | Compilador valida y da error (D3) | - -## Log de errores - -| Error | Intento | Resolucion | -|-------|---------|------------| -| _(se rellenara durante ejecucion)_ | | | diff --git a/tests/run-all.red b/tests/run-all.red index 308fc6b..f608aa7 100644 --- a/tests/run-all.red +++ b/tests/run-all.red @@ -34,6 +34,7 @@ do %test-model.red do %test-topo-sort.red do %test-compiler.red do %test-array.red +do %test-qlib.red ; ── Resumen ────────────────────────────────────────────────────────── total: pass-count + fail-count diff --git a/tests/test-overhead.red b/tests/test-overhead.red new file mode 100644 index 0000000..9b92542 --- /dev/null +++ b/tests/test-overhead.red @@ -0,0 +1,141 @@ +Red [ + Title: "Test overhead — observador pasivo de GTK CSD" + Needs: 'View +] + +; ── Observador pasivo ───────────────────────────────────────────── +; No intenta detectar flips CSD↔cliente ni corregir overhead. +; Mide el overhead inicial una sola vez y lo usa siempre. +; +; Consecuencia: si GTK pasa a modo cliente (alt+tab, o durante un +; maximize/restore), el canvas queda ~98x108 más pequeño de lo que +; podría, con padding visible en el borde derecho/inferior. +; Aceptable: nunca hay overflow ni estado corrompido. +; +; El log pasivo registra cada cambio de face/size para diagnóstico. + +_spec-size: 600x400 +_csd-overhead: 0x0 ; medido al primer on-time, luego fijo +_last-size: 0x0 ; solo para log de deltas +_focus-set?: false +_event-log: copy [] +_tick: 0 +_log-file: %/tmp/test-overhead.log + +write _log-file "" + +canvas: make face! [ + type: 'base + size: 580x380 + offset: 5x5 + color: 240.240.245 + draw: [] +] + +; Sumidero de foco — tiene que ser un field realizado para poder +; recibir set-focus sin que Red peta al navegar con Tab. +tab-sink: make face! [ + type: 'field + size: 1x1 + offset: -100x-100 +] + +log-event: func [label extra /local entry] [ + _tick: _tick + 1 + entry: rejoin ["#" _tick " " label " " extra] + _event-log: head insert _event-log entry + if (length? _event-log) > 12 [ + _event-log: copy/part _event-log 12 + ] + write/append _log-file rejoin [entry newline] +] + +log-size: func [label sz /local dx dy delta] [ + delta: "" + if _last-size <> 0x0 [ + dx: sz/x - _last-size/x + dy: sz/y - _last-size/y + if any [dx <> 0 dy <> 0] [ + delta: rejoin [" Δ=" dx "x" dy] + ] + ] + log-event label rejoin ["win:" sz delta] + _last-size: sz +] + +render-canvas: func [win /local cw ch _n] [ + cw: win/size/x - _csd-overhead/x - 10 + ch: win/size/y - _csd-overhead/y - 10 + if cw < 50 [cw: 50] + if ch < 50 [ch: 50] + canvas/size: as-pair cw ch + canvas/draw: compose [ + pen red line-width 3 + fill-pen off + box 1x1 (canvas/size - 2x2) + pen black + text 10x8 (rejoin ["canvas/size: " canvas/size]) + text 10x26 (rejoin ["win/size: " win/size]) + text 10x44 (rejoin ["csd-overhead:" _csd-overhead " (fijo)"]) + text 10x62 (rejoin ["ticks: " _tick]) + pen gray + line 10x82 (as-pair (cw - 10) 82) + pen blue + text 10x88 "-- EVENT LOG (más reciente arriba) --" + ] + _n: 0 + foreach entry _event-log [ + append canvas/draw compose [ + pen black + text (as-pair 10 (108 + (_n * 16))) (entry) + ] + _n: _n + 1 + ] +] + +win: make face! [ + type: 'window + text: "Test overhead — observer" + size: _spec-size + offset: 100x100 + flags: [resize] + color: white + pane: reduce [tab-sink canvas] + rate: 0:0:0.2 ; timer inicial para forzar primer on-time + actors: make object! [ + on-resize: func [face event] [ + log-size "on-resize" face/size + face/rate: 0:0:0.05 ; diferir render hasta que GTK se asiente + ] + on-time: func [face event] [ + face/rate: none + log-size "on-time" face/size + if _csd-overhead = 0x0 [ + _csd-overhead: face/size - _spec-size + log-event "INIT" rejoin ["csd-ov=" _csd-overhead] + ] + render-canvas face + ; show canvas explícito: GTK3 no siempre propaga shrink + ; desde show face a los hijos. + show canvas + if not _focus-set? [ + _focus-set?: true + set-focus tab-sink + log-event "set-focus" "" + ] + ] + on-focus: func [face event] [ + log-size "on-focus" face/size + ] + on-unfocus: func [face event] [ + log-size "on-unfocus" face/size + ] + on-key: func [face event] [ + ; Consumir Tab — Red/View crashea navegando foco en base faces (GTK-015) + if event/key = #"^-" [return 'done] + if event/key = 'tab [return 'done] + ] + ] +] + +view win diff --git a/tests/test-qlib.red b/tests/test-qlib.red new file mode 100644 index 0000000..95e577b --- /dev/null +++ b/tests/test-qlib.red @@ -0,0 +1,70 @@ +Red [Title: "QTorres — Tests .qlib"] + +do %../src/graph/model.red + +; ── Tests de librería .qlib ────────────────────────────────────────────── + +suite "qlib — load-qlib" + +; Fichero no existente +assert "load-qlib none si fichero no existe" ( + none? load-qlib %/tmp/no-existe.qlib +) + +; Directorio en vez de fichero +assert "load-qlib none si se pasa directorio" ( + none? load-qlib to-file rejoin [form what-dir "../src/"] +) + +; Cargar math.qlib del ejemplo (fichero único) +_qlib-file: to-file rejoin [form what-dir "../examples/math.qlib"] +_q: load-qlib _qlib-file + +assert "load-qlib devuelve objeto para math.qlib" (object? _q) +assert "load-qlib name correcto" (_q/name = "math") +assert "load-qlib version correcta" (_q/version = 1) +assert "load-qlib members no vacío" (not empty? _q/members) +assert "load-qlib members son file!" (file? first _q/members) + +_found-add: false +foreach _m _q/members [if find form _m "add.qvi" [_found-add: true]] +assert "load-qlib add.qvi está en members" _found-add + +_found-sub: false +foreach _m _q/members [if find form _m "subtract.qvi" [_found-sub: true]] +assert "load-qlib subtract.qvi está en members" _found-sub + +_all-exist: true +foreach _m _q/members [unless exists? _m [_all-exist: false]] +assert "load-qlib todos los miembros existen en disco" _all-exist + +suite "qlib — find-qlibs" + +_examples-dir: to-file rejoin [form what-dir "../examples/"] +_libs: find-qlibs/from _examples-dir + +assert "find-qlibs devuelve bloque" (block? _libs) +assert "find-qlibs encuentra math.qlib" (not empty? _libs) +_first-lib: first _libs +assert "find-qlibs primer resultado es objeto" (object? _first-lib) +assert "find-qlibs primer resultado tiene name" (string? _first-lib/name) + +_libs-empty: find-qlibs/from to-file rejoin [form what-dir "../src/"] +assert "find-qlibs devuelve bloque vacío si no hay .qlib" (block? _libs-empty) +assert "find-qlibs vacío si no hay .qlib" (empty? _libs-empty) + +suite "qlib — ejemplo usa-libreria" + +_ejemplo-path: to-file rejoin [form what-dir "../examples/usa-libreria.qvi"] +assert "usa-libreria.qvi existe" (exists? _ejemplo-path) + +_add-path: to-file rejoin [form what-dir "../examples/math/add.qvi"] +assert "math/add.qvi existe" (exists? _add-path) + +_sub-path: to-file rejoin [form what-dir "../examples/math/subtract.qvi"] +assert "math/subtract.qvi existe" (exists? _sub-path) + +_qlib-path: to-file rejoin [form what-dir "../examples/math.qlib"] +assert "math.qlib es fichero (no directorio)" ( + all [exists? _qlib-path not dir? _qlib-path] +) diff --git a/tests/test-window-raise.red b/tests/test-window-raise.red new file mode 100644 index 0000000..4357f12 --- /dev/null +++ b/tests/test-window-raise.red @@ -0,0 +1,64 @@ +Red [ + Title: "QTorres — Test window raise cross-platform" + Purpose: "Verificar si show/view/no-wait eleva una ventana al frente en la plataforma actual" + Needs: 'View +] + +; ── TEST: ¿show face eleva una ventana existente al frente? ────── +; +; INSTRUCCIONES: +; 1. Ejecutar: red-view tests/test-window-raise.red +; 2. Se abren dos ventanas: A (izquierda) y B (derecha). +; 3. Hacer click en la ventana A para que tenga foco. +; 4. Abrir otra aplicación encima de la ventana B (para que quede tapada). +; 5. Pulsar el botón "Traer B al frente" en A. +; +; RESULTADO ESPERADO: +; - Si B sube al frente: `show face` funciona para elevar → NO es bug GTK (o ya fue corregido) +; - Si B NO sube: `show face` no eleva → anotar plataforma en GTK_ISSUES.md +; +; PLATAFORMAS A VERIFICAR: +; [ ] Linux/GTK (comportamiento conocido: NO eleva) +; [ ] Windows +; [ ] macOS + +win-b: none + +win-b: make face! [ + type: 'window + text: "Ventana B — debe subir al frente" + size: 300x200 + offset: 700x200 + pane: reduce [ + make face! [ + type: 'text + text: "Soy la ventana B. ¿Me ves al frente?" + size: 280x180 + offset: 10x10 + ] + ] +] +view/no-wait win-b + +view make face! [ + type: 'window + text: "Ventana A — control" + size: 300x200 + offset: 100x200 + pane: reduce [ + make face! [ + type: 'base + size: 240x40 + offset: 30x80 + color: 50.100.180 + draw: [fill-pen 240.245.250 text 30x12 "Traer B al frente (show)"] + actors: make object! [ + on-down: func [face event] [ + if all [win-b object? win-b] [ + show win-b + ] + ] + ] + ] + ] +]