Description
Qué propone
Abrir y cerrar el panel de detalle con un clic directo sobre el orbe, además del hover actual. Es un cambio de UX multiplataforma, así que va por separado del arreglo de Wayland (#169).
Motivación
Hoy el panel abre por mouseenter con OPEN_DELAY y cierra por mouseleave + temporizador. En Linux eso es frágil:
- No hay una segunda señal que filtre el paso accidental del puntero. El muestreo nativo de presencia (bug(macOS): el popup de detalle permanece visible tras dejar el hover (incluso moviendo el widget) #154) solo existe en macOS porque ahí WebKit deja de emitir
mouseleave; en WebKitGTK los eventos del DOM funcionan, así que no hay motivo para añadirlo. - El único freno es el temporizador de apertura. Con 90 ms el detalle se dispara al recorrer la tira con el cursor; con 350 ms (lo que lleva ahora solo Linux en fix(linux): gates de plataforma y cálculo del arrastre en Wayland (#168) #169) el barrido deja de abrirlo, pero 350 ms también se siente lento cuando querés abrirlo a propósito.
- Un clic es una intención explícita y no depende del recorrido del puntero.
Decisión de producto que hace falta
El clic tiene que coexistir con el hover, y ahí está la pregunta que no quiero resolver por mi cuenta:
- Clic como atajo, hover intacto.
OPEN_DELAYse queda como está; el clic abre al instante. Simple y retrocompatible. El coste: en Linux el hover a 350 ms sigue sintiéndose lento para quien ya se acostumbró al clic. - Clic como interruptor, hover reducido. Un clic sobre un orbe con el panel abierto lo cierra, y sobre otro orbe lo cambia directamente. Es el comportamiento que many desktop apps usan para menús popover. El coste: es un cambio de modelo mental respecto al hover puro, y hay que decidir si el hover sigue abriendo o solo cambia.
- Clic excluyente en Linux. Donde el hover es frágil, el clic es el mecanismo principal y el hover solo cambia entre orbes ya abierto. Es lo más coherente con el diagnóstico, pero diverge por plataforma.
También hay que decidir si el clic debe abrir también el panel ya visible desde el orbe correspondiente (por ejemplo, para cambiar de provider sin cerrar antes).
Por qué no va en #169
Porque no es un arreglo de una plataforma: es una decisión de interacción que afecta a los tres. En #169, OPEN_DELAY quedó acotado a Linux y el toggle por clic fuera, para no mezclar un cambio de comportamiento global con el acotamiento de los recoveries por plataforma.
Alcance de la implementación, si se aprueba
ProviderGaugeacepta unonClickopcional; si no se pasa, el comportamiento actual queda intacto.openFor(id, el, immediate)conimmediatepara saltar el temporizador.- Un test que fije que el clic abre, que el segundo clic sobre el mismo orbe cierra, y que sobre otro orbe cambia.
- Decisión de plataforma documentada en el propio código.
Nota
Mientras tanto, en Linux el OPEN_DELAY de #169 queda en 350 ms. Si esta issue se aprueba con la opción 3, ese valor podría volver a 90 ms para Linux porque el clic pasa a ser la vía principal.
Activity
egdev6 commentedon Oct 22, 2026
Tras valorar las opciones de la issue, propuesta de decisión de producto:
Clic = anclar / desanclar, no abrir
Tenéis razón en que un clic que abre entra en conflicto con el hover (hacen lo mismo, y el hover ya abrió el panel a mitad del recorrido del cursor hacia el clic). En cambio, si el clic hace algo que el hover no puede, ambos mecanismos coexisten sin pelearse:
mouseleave+ temporizador). Sin cambios.Casos de uso que esto habilita y el hover no cubre: copiar un dato del panel, cambiar de provider varias veces sin perseguir el cursor, y acceso por teclado/touch de forma natural.
Notas de implementación (coherentes con el alcance ya descrito en la issue):
ProviderGaugeaceptaonClickopcional;openFor(id, el, immediate)se mantiene para el cambio de orbe anclado.mouseleave: el cierre solo procede si el panel no está anclado. Cuidado condetailInsideRefy con la interacción del clic con el umbral de arrastre del grip (distinguir porpointerupsin movimiento).OPEN_DELAY.El retardo de apertura se resuelve aparte
Para la queja del
OPEN_DELAYlento a propósito, el ajuste de duración de la animación (estético) se ha movido a #171, y el retardo de apertura (conductual) queda pendiente de decidir: los presets de sensibilidad del hover parecen la vía natural, con la ventaja de que un default de ~250 ms podría sustituir al 350 ms por plataforma de #169. Eso sería nota de cierre de #169, no requisito de esta issue.Rafaeldelinares commentedon Oct 29, 2026
@egdev6
Gracias por la propuesta: nos encaja y la tomamos tal cual. El hover sigue abriendo transitorio y el clic ancla/desancla, que además habilita el caso que más nos faltaba (copiar un dato del panel sin perseguir el cursor).
Pedimos asignación para implementarla.
Sobre la decisión que dejás pendiente —el retardo de apertura—, aportamos tres datos medidos sobre
main(4a5363e), para que no tengas que ir a buscarlos:src/lib/os-detect.ts→openDelayForOs()src/Widget.tsx→CLOSE_DELAY,scheduleClose()mouseleave); en Linux el cierre depende del DOMsrc/lib/detail-presence.ts→shouldSampleNativePresence()El efecto que describe Rafael en su uso diario encaja con el tercero: el panel vive en otra ventana nativa, así que al ir del orbe al panel hay que cruzar el hueco; al cruzarlo, la tira dispara su
mouseleavey arranca el cierre, y si el puntero no entra al panel dentro de los 280 ms, el panel se cierra bajo el puntero. En macOS eso lo resuelve el muestreo nativo; en Linux no hay red de contención.Nuestra recomendación: no tocar el retardo dentro de esta issue. Si más adelante hay presets de sensibilidad del hover, el default puede pasar a ~250 ms y sustituir al 350 ms por plataforma (como señalás, como nota de cierre de #179); mientras tanto, 350 ms es lo que evita el barrido accidental hacia otro orbe.
Un dato de contexto que te puede servir: con la corrección de #179 (PR #205, abierta y ya actualizada con el
mainactual) el arrastre bajo X11 mueve la tira y la deja pegada al borde del área de trabajo en nuestra sesión, así que el caso "hover + clic-ancla" es usable también en Linux.egdev6 commentedon Oct 29, 2026
@Rafaeldelinares Gracias por los datos, los comprobé contra
mainy son correctos. Os asigno la issue.Estoy de acuerdo en dejar el retardo de apertura fuera de esta issue: 350 ms en Linux se queda como está, y los presets de sensibilidad del hover irán por separado.
Antes de escribir código, pido que el plan resuelva un punto: cómo se cierra el panel anclado cuando se hace clic fuera. La tira y el panel son ventanas nativas transparentes que no toman el foco. Un clic fuera de las dos le llega a otra aplicación, así que el DOM no se entera. Lo mismo pasa con
Escape, porque esas ventanas no reciben el teclado. Si no hay una señal nativa (pérdida de foco u otra equivalente) por plataforma, el panel anclado puede quedarse abierto de forma indefinida. Me vale una respuesta por plataforma, aunque en alguna la única salida sea el segundo clic sobre el orbe, siempre que quede documentada en el código.Un detalle más: el clic tiene que respetar el umbral de arrastre de los orbes que ya existe (
orbDrag.movedenWidget.tsx), para que reordenar no ancle el panel por accidente. Los tests deberían cubrir ese caso.Rafaeldelinares commentedon Oct 29, 2026
@egdev6 plan para el anclaje por clic, con la respuesta a tu pregunta por plataforma.
Cómo se cierra el panel anclado
Tu pregunta tiene dos partes: qué señal existe para notar el clic fuera, y qué hace el panel mientras está anclado. Empiezo por la segunda, porque condiciona la primera.
El panel anclado nunca es una trampa. Las salidas, siempre disponibles:
Escape: lo atiende la tira, que tiene el foco desde el clic.No añadimos ningún control nuevo al panel: el que ya tiene (ocultar el provider) es otra cosa, y un botón de cerrar sería un control redundante para quien ya conoce el segundo clic.
La señal: la tira pierde el foco
Al hacer clic en un orbe, la ventana de la tira toma el foco (es una ventana normal y focusable;
focus: falseen la config es sólo el arranque). A partir de ahí:WindowEvent::Focused(false)en la ventanawidget⇒ desancamos y cerramos.Esa señal es nuestra (la de una ventana que ya controlamos), no un watch global de la entrada: no requiere
XGrabButtonni tocar el servidor X, no añade dependencias y no estorba a otras aplicaciones.Por qué descartamos la alternativa: un
XGrabButtonpasivo sobre la ventana raíz recibe los clics globales, pero bajo XWayland el servidor X no ve los clics de las aplicaciones nativas, así que no resolvería el caso de GNOME; y a cambio sería un watch global de la entrada mientras el panel está anclado.Qué se puede y qué no se puede detectar
En el caso de que el compositor no dé el foco a la tira, el panel anclado se cierra con el segundo clic sobre el orbe, y eso queda documentado en el código (es el mismo criterio que aceptaste para el caso de una plataforma sin señal).
El clic no debe anclar al reordenar
El umbral ya existe:
orbDragmarcamoveda partir de 6 px (Math.hypot(...) < 6enWidget.tsx), y hay además una guarda de 400 ms tras reordenar (lastReorderAt). El anclaje sólo se activa si el gesto fue un clic y no un arrastre:orbDrag.current?.moved === falsey fuera de la ventana delastReorderAt, con test determinista de que reordenar un orbe no ancla el panel.Qué cambia y qué no
openDelayForOs(350 ms en Linux, como acordamos).Escapese atiende ahí mismo. El teclado pasa a Quorb hasta que el usuario hace clic en otra aplicación; el coste y sus límites por plataforma van en el mensaje siguiente.Verificación prevista
Escape→ se cierra; y el hover sin cambios.Tamaño esperado
Unas cientos de líneas: estado del panel en el frontend, un evento nuevo en el backend y sus tests. Sin dependencias nuevas, sin watch global de la entrada y sin tocar la capa de colocación. Nada del tamaño de #179, que fue cambiar un protocolo nativo entero.
Editado (30-10-2026): la viñeta «Al anclar, la tira conserva el foco (no le quitamos el teclado a la aplicación del usuario)» era falsa: el clic sí se lleva el teclado. Queda corregida arriba. Los cuatro puntos que pediste están respondidos en el mensaje siguiente.
egdev6 commentedon Oct 29, 2026
@Rafaeldelinares gracias por el plan. La dirección es buena: un solo estado para proveedor y anclaje, reglas explícitas frente al hover, el temporizador y el colapso de la tira, la pérdida de foco de una ventana propia en vez de un watch global de la entrada, y el umbral de reordenación cubierto con test (6 px de
orbDrag.movedmás los 400 ms delastReorderAt). Antes de empezar el código, necesito que el plan resuelva cuatro puntos:Windows y macOS. La tabla solo cubre X11, XWayland y Wayland nativo, y la pregunta era por plataforma. Explicad cómo se comporta la pérdida de foco en Windows y en macOS al hacer clic en otra aplicación, teniendo en cuenta que en macOS WebKit ya obligó a muestrear la presencia nativa (bug(macOS): el popup de detalle permanece visible tras dejar el hover (incluso moviendo el widget) #154). Si en alguna plataforma la única salida es el segundo clic, documentadlo igual.
Clic dentro del panel. La ventana de detalle se puede enfocar: en
src-tauri/src/lib.rs(constructor deDETAIL) solo lleva.focused(false), sin el.focusable(false)del overlay. Al hacer clic en el panel para copiar un dato, el foco pasa a esa ventana y la tira recibeFocused(false). Con la regla del plan, el panel anclado se cerraría justo en el caso de uso principal. La condición tiene que ser que el foco salga de las dos ventanas propias, la tira y el detalle, y conviene cubrirlo con un test sobre el seam de foco, no solo con la prueba manual.Foco y teclado. El plan dice que la tira conserva el foco «sin quitarle el teclado a la aplicación del usuario», pero que el clic en el orbe dé el foco a la tira es precisamente quitárselo: de eso dependen
Escapey la señal de pérdida de foco. Me parece un coste aceptable, pero tiene que quedar descrito tal cual, por plataforma. Por ejemplo: «tras anclar, lo que se escribe va a Quorb hasta hacer clic en otra aplicación, y ese clic cierra el panel».Escapecon el foco en el panel. Si el usuario hizo clic dentro del panel, el foco está en la ventana de detalle:Escapetiene que cerrar el panel anclado también desde ahí, no solo desde la tira.Con esos cuatro puntos en el plan, adelante con la implementación.
Rafaeldelinares commentedon Oct 30, 2026
@egdev6 los cuatro puntos, respondidos con el código a la vista. Y una corrección de fondo: en el mensaje anterior escribí que «la tira conserva el foco (no le quitamos el teclado a la aplicación del usuario)». Es falso —el clic se lleva el teclado— y ya lo he corregido en ese mensaje. Aquí va la respuesta completa.
1. Windows y macOS
La señal es la misma en las tres plataformas:
WindowEvent::Focused(false)en una ventana propia. Lo que cambia es el evento nativo que la produce, y eso lo he leído entao 0.37.1, el crate que usa el proyecto:Focused(false)focus_out_eventde GTKtao-0.37.1/src/platform_impl/linux/event_loop.rs:655-664focus_out_event, que GTK emite cuando el compositor revoca el focoWM_NCACTIVATEconwparam = 0yWM_KILLFOCUS→lose_active_focustao-0.37.1/src/platform_impl/windows/event_loop.rs:1729-1757y825-850windowDidResignKeytao-0.37.1/src/platform_impl/macos/window_delegate.rs:384-411Y dos precisiones que salen del mismo código:
taohace quecanBecomeKeyWindowdevuelva el atributofocusable(tao-0.37.1/src/platform_impl/macos/window.rs:415-424): la tira y el detalle lo son por defecto —sólo el overlay llevafocusable(false),src-tauri/src/lib.rs:1072—, así que las dos pueden recibir el foco. En Linux, la tira nace conaccept_focus = falseporque se crea confocus: false, perotaolo restituye atrueen el primer dibujo (tao-0.37.1/src/platform_impl/linux/window.rs:90y230-240): la tira acepta el foco en cuanto está visible, y el clic se lo da.mouseleave) que WebKit deja de entregar en ventanas transparentes; por eso el hover usa muestreo nativo de la posición del cursor (src/Widget.tsx:786-828, sólo macOS). El foco es un evento de ventana, no de WebKit: llega igual en las tres plataformas.Qué está verificado y qué no:
«Por construcción» quiere decir exactamente eso: el evento nativo que produce el
Focused(false)está leído en el código detao, pero el recorrido completo no está medido en esas dos plataformas. No lo damos por bueno: en la evidencia de la PR irá como verificado en fuente y pendiente de smoke, y si alguien con esos sistemas lo prueba, mejor.En el caso de que el compositor no conceda el foco a la tira (Wayland nativo es el caso posible), la única salida es el segundo clic sobre el orbe, y queda escrito en el código, como aceptaste para el caso de una plataforma sin señal.
En Wayland nativo ese caso es más probable que en las otras plataformas, y por un motivo que viene de #180/#210: el arrastre lo delega el compositor (
startDragging), así que el gesto no necesita que la tira tenga el foco de teclado. Nada obliga entonces al compositor a dárselo al hacer clic, y de ahí que el comportamiento real del foco bajo Wayland nativo haya que medirlo y no deducirlo.2. Clic dentro del panel
Tienes razón: el detalle es focusable. En
src-tauri/src/lib.rs:1046-1056lleva.focused(false)—que sólo es el arranque— y no lleva el.focusable(false)del overlay (src-tauri/src/lib.rs:1072). Al hacer clic dentro del panel para copiar un dato, el foco pasa a la ventana de detalle y la tira recibeFocused(false).La condición cambia: el anclaje sólo se suelta cuando el foco sale de las dos ventanas propias —la tira y el detalle—. El foco en el detalle es «sigo dentro de mi propia interfaz».
Hay una trampa de orden en las tres plataformas, y por eso la decisión no se toma en el evento sino después: la pérdida de foco de la ventana antigua llega normalmente antes que la ganancia de la nueva (en X11 el gestor envía
FocusOuta la vieja yFocusIna la nueva; en macOSwindowDidResignKeyprecede awindowDidBecomeKey; en WindowsWM_KILLFOCUSprecede aWM_SETFOCUS). Una regla que reaccionase al primerFocused(false)cerraría el panel justo en el caso de uso principal.La solución: guardamos el estado de foco de las dos ventanas y no soltamos nada hasta 150 ms después del último evento de foco; si en ese momento ninguna de las dos ventanas propias tiene el foco, el anclaje se suelta. La transferencia de foco entre nuestras dos ventanas ocurre en el mismo lote de eventos, así que 150 ms sobran; el número va como constante y con su test. El diseño no depende del orden: el settling window es lo que lo hace robusto.
3. Foco y teclado
El coste, dicho sin rodeos: tras anclar, el teclado va a Quorb. Lo que escribes va a la tira, no a la aplicación en la que estabas. Y no lo hacemos artificialmente: no llamamos a
set_focus, no añadimos atajo global ni watch de la entrada; el teclado se mueve porque el usuario ha clicado nuestra ventana.El coste queda acotado por la misma regla que lo produce: el clic que te devuelve a tu aplicación es el clic que cierra el panel. Y si el compositor no da el foco, no hay coste —ni hay señal—.
4.
Escapecon el foco en el panelHoy no hay ningún
Escapeen la tira ni en el panel (ensrc/sólo lo atiendeSettings.tsx), así que esto es nuevo:Escape→ cierra el panel y suelta el anclaje;Escape→ avisa a la tira por el mismo canal que ya usadetail-dismiss(src/lib/detail-panel.ts:8-15) → cierra el panel y suelta el anclaje;Qué cambia y qué no
openDelayForOsno se toca (350 ms en Linux, como acordamos).Escapedesde cualquiera de las dos, ocultar el provider o cerrar la app.orbDrag.moved) y la guarda de 400 ms (lastReorderAt) (src/Widget.tsx:964-1007).Verificación prevista
Frontend (vitest):
Escapeen la tira cierra el panel;Escapeen el panel también.Seam de foco (vitest, con reloj falso):
Nativo: el backend sólo reenvía los eventos de foco de las dos ventanas propias; la decisión y la ventana de 150 ms viven en el seam del frontend, así que se prueban sin servidor X.
Manual (KDE Plasma 6.7.5, X11/XWayland): anclar → clic dentro del panel (copiar un dato) → sigue abierto; clic en otra aplicación → se cierra; segundo clic → se cierra;
Escapedesde la tira y desde el panel → se cierra; el hover sin cambios.Wayland nativo: medimos en esta máquina si el compositor concede el foco a la tira. Si lo concede, el clic fuera cierra el panel; si no, la única salida es el segundo clic, y eso queda en la evidencia de la PR.
Windows y macOS: sin host para medirlo aquí; queda en la evidencia de la PR como verificado en fuente y pendiente de smoke.
Tamaño esperado
Unas cientos de líneas: el seam de foco y sus tests, el reenvío de los eventos en el backend, el estado de anclaje en la tira y el
Escapeen las dos ventanas. Sin dependencias nuevas, sin watch global de la entrada y sin tocar la capa de colocación. Nada del tamaño de #179, que fue cambiar un protocolo nativo entero.egdev6 commentedon Oct 31, 2026
@Rafaeldelinares, el plan responde los cuatro puntos de la revisión anterior y la dirección es buena: foco tratado como conjunto de las dos ventanas, Escape desde ambas y coste de teclado descrito explícitamente. Para avanzar, dejemos estas precisiones incorporadas al plan y sus tests:
onPointerUplimpiaorbDrag.currentantes declick, por lo que comprobar allímoved === falseno alcanza. Cubrir clic normal, arrastre de reordenación y activación por teclado, respetando también la guarda posterior al reordenamiento.La distinción entre foco nativo y eventos DOM de ratón es correcta. Mantengamos Windows/macOS como pendientes de smoke: la lectura de tao acredita la señal disponible, pero no el recorrido completo. Con estas precisiones, adelante con la implementación dentro del alcance acordado, sin modificar los retardos de apertura.
fix(widget): cancelar aperturas pendientes al cerrar con Escape (egde…
docs(i18n): regenerar el inventario de textos (egdev6#170)
fix(widget): preservar el autocierre de paneles transitorios al perde…
test(widget): importar PanelState y regenerar inventario i18n (egdev6…
docs(i18n): regenerar el inventario de textos (egdev6#170)
feat(widget): el clic sobre el orbe ancla el panel de detalle (#170) (#…