feat(quickshell): keep only one popout open across all screens
Update PKGBUILD version / update-pkgver (push) Successful in 6s

Each bar owns a per-screen PopoutState, so on multi-monitor a popout
could stay open on one screen while another opened on the next — and
per-screen `shown` drives global services (bluetooth discovery, wifi
scan), so closing one screen's popout could kill the service under the
other's still-open popout.

A small PopoutBus singleton holds an `owner` token: open() claims it,
and any state that loses ownership folds up. Per-screen state (and thus
per-screen anchoring/rendering) stays; only cross-screen exclusivity is
added.
This commit is contained in:
2026-07-06 23:34:55 +02:00
parent a7bf8fe37b
commit c34d1d29fe
4 changed files with 31 additions and 1 deletions
+6
View File
@@ -200,3 +200,9 @@
- **Why**: The composition switch left `Popouts.qml` a hardcoded `switch` — a user-composed widget could join the bar but couldn't open a popout in the shared animated overlay, so it was second-class (a widget without a popout is useless for most cases). quickshell's native `PopupWindow` would be composable by construction but drops moonarch's unfold/morph animation (one shared overlay) — decided: **keep the animation**, make the registry composable instead. caelestia's central `Panels.qml` + `DrawerVisibilities` was considered and rejected: it fits a handful of author-owned edge drawers, not many user-composable per-widget popouts (it reintroduces central registration).
- **Tradeoffs**: `open()` takes a `Component` instead of a name — less conventional, but it removes the central registry entirely: a widget carries its own popout and renders it in the shared morphing frame. Churn across the 10 trigger widgets + `open()` signature; each widget file gains `pragma ComponentBehavior: Bound` so the inline popout `Component` resolves `root` when the shared host instantiates it.
- **How**: `PopoutState.currentName``currentComponent`; `open(component, rect, payload)`. `PopoutHost` renders `Loader { sourceComponent: popouts.currentComponent }`. `Popouts.qml` deleted, `qmldir` regenerated. Each of the 10 widgets declares an inline `Component { XxxPopout { popouts: root.popouts } }` (Tray also wires `menuHandle: popouts.payload`) and calls `open(thatComponent, …)`. Verified: `qmllint` clean, and an end-to-end throwaway config where a user widget opens its **own** popout on startup instantiates in the shared host with no errors/binding loops. Visual/animation unverified (needs a live eyeball).
## 2026-07-06 Quickshell bar: one popout system-wide (cross-screen exclusivity)
- **Who**: Dominik, ClaudeCode
- **Why**: Each `Bar` owns a per-screen `PopoutState`, so on a multi-monitor setup a popout could stay open on screen 1 while another opened on screen 2. Worse, popout `shown` drives *global* singleton services (`adapter.discovering`, `wifiDev.scannerEnabled`), so closing one screen's popout could stop the service under another screen's still-open popout. A popout is a single focused interaction — only one should exist at a time.
- **Tradeoffs**: Kept the per-screen `PopoutState` (rendering + anchor coordinates are screen-local; a single global state would either mirror the popout onto every monitor or need a screen field threaded through `open()`/`PopoutHost`). Added only the minimal cross-screen coordination — one `owner` token. Rejected a central registry (the composable-popouts decision just removed one). Not a compositor grab: pure QML state, so it never preempts other windows.
- **How**: New `PopoutBus.qml` singleton with `property var owner`. `PopoutState.open()` sets `PopoutBus.owner = root`; `close()` clears it if still owned. A `readonly property bool ownsSlot: PopoutBus.owner === root` + `onOwnsSlotChanged` folds up any state that loses the slot (no loop: a foreign-close leaves `owner` untouched). `qmldir` regenerated (adds `singleton PopoutBus`). Verified: throwaway `quickshell -p …/moonarch``Configuration Loaded`, no binding loop/error. Cross-screen fold + visuals unverified (needs a live multi-monitor eyeball).
@@ -0,0 +1,10 @@
// ABOUTME: System-wide coordinator so only one bar popout is open at a time across all screens.
// ABOUTME: Each per-screen PopoutState claims `owner` on open; the others fold up when ownership changes.
pragma Singleton
import QtQuick
import Quickshell
Singleton {
// The PopoutState that currently owns the single, system-wide open popout (null = none).
property var owner: null
}
@@ -1,5 +1,6 @@
// ABOUTME: Shared state for a bar's popout host — which popout is open, where, and its payload.
// ABOUTME: One instance per bar (per screen); bar widgets call open()/close().
// ABOUTME: One instance per bar (per screen); bar widgets call open()/close(). Only one may be open
// ABOUTME: system-wide — cross-screen exclusivity is coordinated through the PopoutBus singleton.
import QtQuick
QtObject {
@@ -15,9 +16,21 @@ QtObject {
root.anchorRect = rect;
root.currentComponent = component;
root.hasCurrent = true;
// Claim the system-wide slot; any popout open on another screen folds up.
PopoutBus.owner = root;
}
function close() {
// Deliberately leaves currentComponent set: PopoutHost's Loader keeps that
// instance alive and sized through the fold-up animation. Nulling it here would
// unload the content instantly and collapse the slide-out to zero size.
root.hasCurrent = false;
if (PopoutBus.owner === root)
PopoutBus.owner = null;
}
// Fold up when another screen's popout claims the system-wide slot.
readonly property bool ownsSlot: PopoutBus.owner === root
onOwnsSlotChanged: if (!root.ownsSlot && root.hasCurrent)
root.close()
}
+1
View File
@@ -34,6 +34,7 @@ singleton IdleInhibit IdleInhibit.qml
singleton NightlightService NightlightService.qml
singleton Niri Niri.qml
singleton NotifService NotifService.qml
singleton PopoutBus PopoutBus.qml
singleton Theme Theme.qml
Toggle Toggle.qml
Tooltip Tooltip.qml