Panes do not all close the same way, and none close on Escape #15

Closed
opened 2026-08-16 22:20:24 +01:00 by joshquinlan · 3 comments
Owner

Escape closes nothing. Only the notification centre listens for
it, because it is the one surface that takes keyboard focus.

Behind that, the same pane exists in three shapes:

  • PanelSurface owns a frame and a PopoverGrab, and six panes use
    it (network, audio, bluetooth, vpn, microphone, clock).
  • PowerPanel builds its own PopupWindow, its own frame and its
    own grab rather than using PanelSurface.
  • TrayMenuPopup does the same again.

The bookkeeping is copied too. PanelToggle watches its surface
to call Panels.noteClosed(), and BatteryIndicator repeats that
same Connections block by hand because PowerPanel is not wired
through PanelToggle. ClockDisplay skips Panels altogether: a raw
MouseArea flips dropdown.visible, so opening the clock leaves
another pane open beside it.

The shape to land on, one class and many instances:

  • Panels holds which pane is open, as a name rather than a
    visible flag scattered across surfaces: toggle(name),
    show(name), close(). Opening one pane closing another falls
    out of that instead of being arranged.
  • PanelSurface takes a name, binds its own visibility to
    Panels, owns the grab, and answers Escape. Every pane is a
    PanelSurface, PowerPanel, the tray menu and the clock
    included.
  • PanelToggle shrinks to reporting a click.

Escape needs deciding: a PopupWindow takes no keyboard focus, so
whether the focus grab delivers the key at all has to be tested
before choosing between handling it in the surface and having
the compositor tell the shell.

Escape closes nothing. Only the notification centre listens for it, because it is the one surface that takes keyboard focus. Behind that, the same pane exists in three shapes: - PanelSurface owns a frame and a PopoverGrab, and six panes use it (network, audio, bluetooth, vpn, microphone, clock). - PowerPanel builds its own PopupWindow, its own frame and its own grab rather than using PanelSurface. - TrayMenuPopup does the same again. The bookkeeping is copied too. PanelToggle watches its surface to call Panels.noteClosed(), and BatteryIndicator repeats that same Connections block by hand because PowerPanel is not wired through PanelToggle. ClockDisplay skips Panels altogether: a raw MouseArea flips dropdown.visible, so opening the clock leaves another pane open beside it. The shape to land on, one class and many instances: - Panels holds which pane is open, as a name rather than a visible flag scattered across surfaces: toggle(name), show(name), close(). Opening one pane closing another falls out of that instead of being arranged. - PanelSurface takes a name, binds its own visibility to Panels, owns the grab, and answers Escape. Every pane is a PanelSurface, PowerPanel, the tray menu and the clock included. - PanelToggle shrinks to reporting a click. Escape needs deciding: a PopupWindow takes no keyboard focus, so whether the focus grab delivers the key at all has to be tested before choosing between handling it in the surface and having the compositor tell the shell.
Author
Owner

Implemented in 7176158.

Panels holds one name for what is open. A surface binds its own
visibility to it, so one pane closing another is the state
rather than an arrangement between neighbours. PowerPanel, the
tray menu and the clock are all PanelSurface now, and
PanelToggle is down to reporting a click. BatteryIndicator's
hand-copied Connections block is gone.

Escape arrives through PaneKeys: one layer surface, no size,
nothing drawn, alive only while a pane is open, holding the
keyboard because a PopupWindow is never given it. PopoverGrab
counts it as part of the pane, or taking the keyboard would read
as focus leaving and close the pane on the way in.

Verified: the state machine through the IPC (opening one pane
replaces another, toggling the same one closes it), the key
surface appearing and disappearing with the pane in hyprctl
layers, the pane surviving that surface taking the keyboard, and
the power pane rendering correctly through the shared frame.

Not verified: the tray menu, which changed shape most. Nothing
is registered in the tray at the moment, so RegisteredStatus
NotifierItems is empty and there was no menu to open.

Implemented in 7176158. Panels holds one name for what is open. A surface binds its own visibility to it, so one pane closing another is the state rather than an arrangement between neighbours. PowerPanel, the tray menu and the clock are all PanelSurface now, and PanelToggle is down to reporting a click. BatteryIndicator's hand-copied Connections block is gone. Escape arrives through PaneKeys: one layer surface, no size, nothing drawn, alive only while a pane is open, holding the keyboard because a PopupWindow is never given it. PopoverGrab counts it as part of the pane, or taking the keyboard would read as focus leaving and close the pane on the way in. Verified: the state machine through the IPC (opening one pane replaces another, toggling the same one closes it), the key surface appearing and disappearing with the pane in hyprctl layers, the pane surviving that surface taking the keyboard, and the power pane rendering correctly through the shared frame. Not verified: the tray menu, which changed shape most. Nothing is registered in the tray at the moment, so RegisteredStatus NotifierItems is empty and there was no menu to open.
Author
Owner

Two findings, one of them the actual bug.

Clicking away never worked, before my changes or after. The grab
was armed in the same pass that made the surface visible, so the
compositor refused a grab for a surface it had not mapped yet,
and a refused grab is silent: it never reports a click landing
elsewhere. Fixed in 6cb15e4 by arming a beat later, with the
grab's state readable through 'panels grabbed' so the difference
between refused and quiet is one question away.

PaneKeys was worse than the problem. Holding the keyboard
exclusively pinned focus to it, so the compositor never saw
focus leave, the grab never cleared, and a pane could not be
dismissed by any means. Removed.

Escape is therefore still open, and wants a route that does not
touch focus: most likely a non-consuming Hyprland bind added
while a pane is open and removed when it closes, so Escape still
reaches applications the rest of the time.

Two findings, one of them the actual bug. Clicking away never worked, before my changes or after. The grab was armed in the same pass that made the surface visible, so the compositor refused a grab for a surface it had not mapped yet, and a refused grab is silent: it never reports a click landing elsewhere. Fixed in 6cb15e4 by arming a beat later, with the grab's state readable through 'panels grabbed' so the difference between refused and quiet is one question away. PaneKeys was worse than the problem. Holding the keyboard exclusively pinned focus to it, so the compositor never saw focus leave, the grab never cleared, and a pane could not be dismissed by any means. Removed. Escape is therefore still open, and wants a route that does not touch focus: most likely a non-consuming Hyprland bind added while a pane is open and removed when it closes, so Escape still reaches applications the rest of the time.
Author
Owner

Confirmed working. Clicking anywhere else closes a pane, and
every pane is one class with instances now.

Escape is not done and is not part of this any more: split out
so closing this does not lose it.

Confirmed working. Clicking anywhere else closes a pane, and every pane is one class with instances now. Escape is not done and is not part of this any more: split out so closing this does not lose it.
Sign in to join this conversation.
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
joshquinlan/dotfiles#15
No description provided.