Skip to content

feat(widget): el clic sobre el orbe ancla el panel de detalle (#170) - #223

Merged
egdev6 merged 11 commits into
egdev6:mainfrom
Rafaeldelinares:feat/170-click-anchor-panel
Oct 31, 2026
Merged

egdev6 merged 11 commits into
egdev6:mainfrom
Rafaeldelinares:feat/170-click-anchor-panel

Conversation

@Rafaeldelinares

Copy link
Copy Markdown
Contributor

Related issue / Issue relacionada

Issue: #170 (feat(ux): abrir y cerrar el detalle con clic directo sobre el orbe, type:feat, status:approved, asignada a @Rafaeldelinares)
ODD: odd/tasks/click-anchor-panel.md — alcance, plan y bitácora.
Plan publicado: #170 (comment) (responde los cuatro puntos que pidió la review del mantenedor).

Scope / Alcance

El clic sobre un orbe ancla el panel de detalle: deja de cerrarse al apartar el puntero, y se suelta con el segundo clic sobre el mismo orbe, con Escape, o cuando el foco sale de nuestras dos ventanas.

  • Un solo estado ({ providerId, anchored }) en vez de dos banderas: el hover, el temporizador de 8 s, el clic y la pérdida de foco compiten por el mismo valor, así que no pueden discrepar.
  • La señal es el foco de las dos ventanas propias (la tira y el detalle), reenviado por el backend desde WindowEvent::Focused. El detalle es focusable (src-tauri/src/lib.rs: lleva .focused(false) —que es sólo el arranque— y no el .focusable(false) del overlay), así que hacer clic dentro para copiar un dato mueve el foco a esa ventana: por eso la condición es que el foco salga de las dos.
  • La decisión se difiere 150 ms desde el último evento de foco: la pérdida de la ventana antigua llega antes que la ganancia de la nueva (X11 FocusOut/FocusIn, macOS windowDidResignKey/windowDidBecomeKey, Windows WM_KILLFOCUS/WM_SETFOCUS) y una regla que reaccionase al primer aviso cerraría el panel justo en el caso de uso principal.
  • Escape cierra el panel desde la tira y desde el panel.
  • Reordenar no ancla: el umbral de 6 px (orbDrag.moved) se consume en pointerup, así que la guarda de 400 ms tras reordenar (lastReorderAt) es la que frena el clic.
  • El hover no abre otro provider con el panel anclado: el anclaje lo eligió un clic y el puntero no lo cambia por debajo.

Fuera de alcance: el hover, su retardo (openDelayForOs no se toca), el temporizador de 8 s, la colocación nativa, la geometría y cualquier comportamiento de Windows o macOS que no sea el del foco.

Coordination / Coordinación

  • Para ODD: asignaciones y solapamientos revisados; toma verificada o acuerdo explícito del mantenedor enlazado.
  • For ODD: issue/PR assignees and overlapping work checked; claim verified, or explicit maintainer coordination linked. — N/A (esta PR se responde en español).

Contributor / Contribuidor: @Rafaeldelinares
Claim or agreement evidence / Evidencia de asignación o acuerdo: la issue #170 está asignada a @Rafaeldelinares y etiquetada status:approved desde el 29-10-2026; el mantenedor pidió los cuatro puntos del plan y los respondió el 30-10-2026 (enlazado arriba). El merge es otra decisión.
Use one language; mark non-ODD items N/A. / Usa un idioma; marca N/A si no aplica ODD.

Acceptance and validation / Aceptación y validación

  • Criteria satisfied / Criterios cumplidos:
    • El clic ancla; el segundo clic sobre el mismo orbe desancla y cierra; el clic sobre otro orbe cambia de provider manteniendo el anclaje.
    • El hover y el temporizador de 8 s no cierran un panel anclado; la tira no se colapsa mientras esté anclado.
    • El anclaje se suelta cuando el foco sale de las dos ventanas propias, con la ventana de asentamiento de 150 ms.
    • Escape cierra el panel desde la tira y desde el panel.
    • Reordenar no ancla.
  • Checks actually run and results / Comprobaciones ejecutadas y resultados:
    • npx vitest run --dir src → 65 ficheros, 807 tests, 0 fallos (19 nuevos: las reglas del panel y el seam de foco).
    • RED de los tests nuevos antes de implementar: fallan por el módulo inexistente; GREEN después: 19 passed.
    • npm test (vitest + node --test) → vitest 807 ok + 298 node ok, 0 fallos.
    • npm run typecheck (tsc --noEmit) → limpio.
    • cargo test --locked → 384 ok / 0 fallos / 3 ignorados.
    • cargo fmt --all -- --check → limpio (gate de chore(tooling): adoptar rustfmt con gate de CI de formato (#184) #185).
    • node scripts/i18n-inventory.mjs --check → limpio (sin textos nuevos; el inventario se regenera en commit aparte, como manda el repo).
    • Tamaño del diff: +556 / −30 en 11 ficheros (código, tests y artefacto ODD).
  • Checks not run and why / Comprobaciones no ejecutadas y motivo:
    • Wayland nativo: en esta máquina no hay forma de inyectar el puntero en Wayland (ydotool, wtype y libei no están instalados) y con el puntero real no se puede automatizar. La medición del foco bajo Wayland nativo queda como el smoke pendiente de feat(linux): hacer funcional el escritorio y widget nativos #107. Lo que se acredita abajo es el comportamiento bajo X11/XWayland, que es donde el compositor mueve el foco al pulsar en otra aplicación.
    • Automatizar el ratón: la inyección sintética (XTest) no despierta el mouseleave del webview de forma consistente, así que la secuencia se hizo con el puntero real y se documenta con capturas (filmstrip de 1 fps + el estado de las ventanas nativas cada 200 ms).
  • Remaining blockers / Bloqueos pendientes:

Evidencia manual (X11/XWayland)

Entorno: KDE Plasma 6.7.5, sesión Wayland (XDG_SESSION_TYPE=wayland), app en X11/XWayland (GDK_BACKEND=x11), panel eDP-1 1920×1080. AppImage de esta rama compilado en el devcontainer (el mismo GTK con el que se construyó el build operativo). El binario base es ~/proyectos/quorb/Quorb_0.8.0-local.AppImage (el árbol de #179, sin este cambio), ejecutado con el mismo backend y en la misma sesión.

# Acción Con este cambio Sin el cambio (base)
1 Mouse sobre un orbe el panel se abre el panel se abre
2 Clic en el orbe el panel queda anclado sigue siendo transitorio
3 Mouse fuera de la tira el panel sigue abierto el panel se cierra (a los 280 ms)
4 Clic dentro del panel (para copiar un dato) el panel sigue abierto; el foco pasa a la ventana de detalle —
5 Clic en otra aplicación el panel se cierra: el foco salió de las dos ventanas propias —
6 Clic en el orbe y clic otra vez en el mismo orbe se desancla y se cierra —
7 Escape con la tira enfocada el panel se cierra —
8 Escape con el foco dentro del panel el panel se cierra —
9 Arrastrar un orbe (reordenar) no ancla —

El paso 3 es la diferencia observable: en el base el panel se cierra al salir el puntero a los 280 ms; con este cambio el panel sigue abierto con el puntero fuera.

Capturas de validación empírica adjuntas:

Una salvedad honesta del paso 9: al soltar el arrastre, el puntero queda sobre el orbe que vuelve a su sitio y el hover vuelve a abrir el panel (transitorio). Es del camino de reordenado, que este cambio no toca (y por eso la fila 9 dice «no ancla» y no «no abre»); lo reporto aparte para que lo decida el mantenedor: #222.

An open PR does not mean integration. Release, secrets and signing operations require separate authorization.
Una PR abierta no implica integración. Release, secretos y firma requieren autorización separada.

…#170)

El clic deja de abrir el panel como un hover más: lo ancla. Un solo
estado dice qué provider se ve y si está anclado, así que el hover, el
8 s timer, the click and the focus loss compete for the same value
instead of carrying parallel flags. With the panel anchored the hover
neither closes it nor moves it to another provider: the anchor was
chosen with a click, and the pointer does not change it from under.

The anchor is released when the focus leaves BOTH of our own windows
(the strip and the detail). The detail is focusable, so clicking inside
it to copy a value moves the focus to that window and the panel must
not close for that. The signal is the native window event
(WindowEvent::Focused), which arrives the same way on X11, Wayland,
Windows and macOS and does not depend on WebKit delivering mouse
events. The decision is not taken on the event but 150 ms later: the
focus loss of the old window arrives before the gain of the new one,
and with the real order a rule reacting to the first notice would close
the panel in the main use case.

Escape closes the panel from the strip and from the panel. Reordering
does not anchor: the click arrives after the pointer is released, and
the 400 ms guard after a reorder stops it.

The deterministic tests cover the state rules and the focus seam (the
repo has no DOM environment, so the wiring is evidenced by the manual
run).
@egdev6

egdev6 commented Oct 31, 2026

Copy link
Copy Markdown
Owner

Gracias por el trabajo, @Rafaeldelinares. Revisando 721cdd2 encontré un punto para corregir antes de aprobar: en src/Widget.tsx, Escape cierra el estado del panel, pero no cancela la apertura pendiente del hover.

P2: reapertura después de Escape. Al enfocar un orbe con teclado se programa la apertura; si se pulsa Escape antes de que venza el retardo, el estado queda cerrado, pero el temporizador sigue activo y después vuelve a abrir el panel sin nueva interacción. Lo reproduje ejecutando los handlers openFor y onKey extraídos de este commit con estado y temporizadores controlados, no en GUI: tras Escape quedó una apertura pendiente y, al ejecutarla, el provider volvió a quedar abierto como transitorio.

Propongo cancelar las aperturas pendientes al cerrar con Escape y cubrir la secuencia foco → Escape antes del retardo → avance del temporizador, comprobando que el panel permanece cerrado. Conviene revisar también el cierre recibido por DETAIL_EVENTS.escape. Los cinco checks de CI están verdes, pero las pruebas del estado puro no cubren este cableado. No ejecuté la suite completa ni validación nativa en esta revisión.

…v6#170)

Al enfocar o pasar el ratón por un orbe se programa la apertura con
retardo; si se pulsaba Escape antes de vencer el retardo, el panel se
cerraba pero el temporizador seguía activo y volvía a abrir el panel.

Se cancelan los temporizadores al recibir Escape (tanto desde la tira
como desde la ventana de detalle) y al perder el foco de ambas ventanas,
y se cubre de forma determinista con .
@egdev6

egdev6 commented Oct 31, 2026

Copy link
Copy Markdown
Owner

Gracias por la corrección, @Rafaeldelinares. Revisé 2b0e37e: el punto anterior de Escape y la apertura pendiente por hover queda resuelto. Encontré otra regresión en el callback de pérdida de foco.

P2: se cancela el autocierre de los paneles transitorios. En src/Widget.tsx:779–783, el callback cancela closeTimer incondicionalmente, pero solo cierra el panel si está anclado. Si se abre un transitorio mediante foco de teclado, se saca el puntero y se cambia de aplicación antes de que venza el cierre de 280 ms, la comprobación de foco puede cancelar ese temporizador y conservar el panel abierto. También elimina el autocierre de ocho segundos del atajo. Lo reproduje ejecutando el callback extraído de este commit con estado y temporizadores controlados: quedó un panel transitorio abierto sin temporizador pendiente, no fue una reproducción GUI.

Propongo preservar el autocierre de los transitorios al soltar el anclaje y añadir una prueba del cableado real. Los tests nuevos de createPanelCoordinator no acreditan este camino porque Widget no utiliza ese coordinador. En el momento de esta revisión el head no tenía checks reportados; no ejecuté suites locales ni validación nativa.

…r foco (egdev6#170)

El callback de pérdida de foco solo debe cancelar temporizadores y cerrar
si el panel está anclado. Si el panel es transitorio (abierto por hover o
atajo), la pérdida de foco no cancela su temporizador de cierre (280 ms o
los 8 s del atajo), preservando su ciclo de vida natural.
@egdev6

egdev6 commented Oct 31, 2026

Copy link
Copy Markdown
Owner

Gracias por atender las revisiones, @Rafaeldelinares. Revisé ffee94d: el problema del autocierre transitorio queda resuelto. La pérdida de foco conserva los temporizadores de los paneles transitorios y solo los limpia al cerrar un panel anclado. Lo comprobé ejecutando el callback real con estado y temporizadores controlados para los cierres de 280 ms y ocho segundos. La corrección anterior de Escape sigue presente.

No encontré nuevos hallazgos bloqueantes y los cinco checks están verdes. Como mejora no bloqueante, convendría cubrir el cableado real del componente: el test nuevo reproduce la lógica de estado y los tests del coordinador no ejercitan su integración con Widget.

No ejecuté suites completas ni GUI. El verificador independiente no pudo arrancar por ENOSPC y no lo reintenté; esta revisión se completó directamente sobre el diff y las fuentes del commit. Por mi parte, recomiendo aprobar; no he emitido una aprobación formal ni realizado el merge. ¡Gracias por el trabajo y las correcciones!

egdev6
egdev6 previously approved these changes Oct 31, 2026
@Rafaeldelinares

Copy link
Copy Markdown
Contributor Author

@egdev6, corregido e integrado en el head 4816ec2:

  1. Preservación del autocierre de paneles transitorios:
    • En src/Widget.tsx, el callback de foco (createFocusTracker) ahora comprueba primero panelHoldsOpen(current): solo cancela closeTimer y openTimer y cierra el panel si estaba anclado.
    • Si el panel es transitorio (abierto por hover o atajo sin anclar), la pérdida de foco no cancela su temporizador, de modo que el autocierre programado (280 ms o los 8 s del atajo) sigue su curso normal sin quedarse colgado.
  2. Tests de cobertura:
    • Añadida prueba en src/lib/anchor-panel.test.ts verificando que la lógica de pérdida de foco cierra el anclado pero deja inalterado el transitorio.
  3. Rama sincronizada con main:

npm test: 847 tests en 67 ficheros en verde (0 fallos).

@egdev6
egdev6 merged commit 4d21f66 into egdev6:main Oct 31, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants