Windows and popups
Every surface the shell draws — panel, background, overlay, OSD, badge, selection surface,
lock screen, popup — is a WindowEntry in the one root WindowRegistry, reachable
through WindowRegistry.of(context). The wlr-layer-shell bridge is the layer_shell git
dependency, which also re-exports the @internal SDK windowing pieces.
The registry is reconciled, never rebuilt
Section titled “The registry is reconciled, never rebuilt”The root’s set of entries is diffed against the live ones, keyed on controller. A declarative
list returned from build would take every module’s popup down with it on any rebuild.
Survivors keep the builder they were created with, so builders read the root’s fields rather than capturing them. Entries render unkeyed, which means dropping one shifts every later one down a slot — hence the registration order: per-monitor surfaces, then overlays, then popups last.
Destroying a native window
Section titled “Destroying a native window”WindowTeardown is the only correct path, and every teardown goes through it. It unmaps,
polls viewIsAttached once per frame, then waits one more. It schedules its own frames —
an idle shell produces none — destroys anyway at maxFrames, and re-checks isDestroyed.
Lock passes hide: false.
Layer-shell rules
Section titled “Layer-shell rules”A full-screen layer-shell surface must call spanFullOutput. The default exclusive zone
of 0 means “move me so I don’t occlude surfaces that reserved space”, so the compositor
otherwise shrinks a full-screen surface into the gap between the panels.
Panels take no keyboard focus; a popup that types borrows it. Panels are
LayerShellKeyboardMode.none, which is what stops the bar pulling focus off whatever the
user is typing in. Interactivity is inherited by popups, so openPopup(needsKeyboard:) flips
the panel to onDemand and back on close and from dispose. Only a borrow that actually
flipped is given back, and the flip is force-committed or it sits queued on a mapped surface.
The desktop surface does the same for an icon rename.
A margin is native, never a Flutter Padding. With no input-region support, an inset
inside a full-size surface swallows every click in the gap. The exclusive zone stays the panel
height: per wlr-layer-shell the zone includes the margin, so adding it would reserve it
twice.
Popups
Section titled “Popups”Nothing in the stack says a popup should go away. The Linux popup controller takes no
gdk_seat_grab, so no popup_done ever arrives, and no layer-shell surface reports focus
loss. PopupCoordinator.instance is the only thing that closes them, and PopupDismissArea
the only thing that notices an outside click.
- A handle’s chain is itself plus its transitive parents, and nothing in a chain dismisses
anything else in it. Parentage is read from
TransientScope.maybeOf(context), never passed by a call site — which is what makes theTransientScopewrapper in the entry builder load-bearing. - The coordinator requests a graceful close (the owner’s
onDismiss), nevercloseLayerWindowitself, so exit animations still play. TransientPolicyis two booleans.dismissesOthers: falseis a hover tooltip;dismissable: falseis a consent prompt, because dismissing the screencast picker is a denial. Refusal is inherited down a chain.- The reopen guard exists because
PopupDismissArea’s ancestorListenerfires before any descendant recognizer — without it, no bar popup could be dismissed by its own button. It is primary-button only, keyed on the hostState. - A closing popup outlives the host that closed it.
closePopupdrops every reference synchronously, so close-then-open in one gesture works, and moves the handle, entry, window andonClosedinto a record that finishes on the animation or a timer, and outright ondispose.
Two clicks the coordinator cannot catch by construction: one on an ordinary application window (there is no grab), and one on bare desktop with no background surface. A full-screen invisible barrier is not the answer — with no input-region support it would swallow every click on the monitor.
PopupTransition plays one controller forward to open and backward to close, so an effect
cannot describe an opening it has no closing for. The effect comes from the theme
(popup_animation), snapshotted at open; PopupEffect.none wraps nothing and creates no
controller.
Popup content lays out under its own FlutterView, so it must carry its own ShellTextRoot —
Directionality.of is a null-assert in release too.