import QtQuick import Quickshell import Quickshell.Hyprland import Quickshell.Wayland import qs Variants { id: variants model: Quickshell.screens Scope { id: unit required property var modelData PanelWindow { id: layer PaletteFade { active: layer.visible } readonly property bool suppressed: Prefs.widgetHideFullscreen && layer.windowUp readonly property var workspaceMonitor: Hyprland.monitorFor(layer.screen) readonly property var activeWorkspace: workspaceMonitor ? workspaceMonitor.activeWorkspace : null readonly property bool windowUp: { const workspace = layer.activeWorkspace; if (!workspace || !workspace.toplevels) return false; return workspace.toplevels.values.some((t) => { const o = t.lastIpcObject || {}; if (o.mapped === false || o.hidden === true) return false; if (o.size && (o.size[0] <= 0 || o.size[1] <= 0)) return false; return (o.fullscreen || 0) > 0 || o.floating === false; }); } screen: unit.modelData visible: Prefs.loaded && Widgets.loaded && (deck.panelLive || ((Prefs.widgetsEnabled || deck.stowing) && Widgets.count > 0 && !layer.suppressed)) color: "transparent" // reserves nothing and refuses to be shrunk into the bar and dock's strips, // so a card can be dragged to a true screen edge and sit under the dock. // setting exclusiveZone at all would silently undo this exclusionMode: ExclusionMode.Ignore // the widget panel lifts the whole board over the windows until it has slid away WlrLayershell.layer: deck.panelLive ? WlrLayer.Overlay : (Prefs.widgetOnTop ? WlrLayer.Top : WlrLayer.Bottom) WlrLayershell.keyboardFocus: deck.panelUp ? WlrKeyboardFocus.Exclusive : (deck.wantsKeyboard ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None) anchors { top: true bottom: true left: true right: true } // lastIpcObject only moves when something asks it to, so nudge it on // the events that can change whether a tiled or fullscreen window exists. Connections { function onRawEvent(event) { if (event.name === "fullscreen" || event.name === "activewindowv2" || event.name === "closewindow" || event.name === "openwindow" || event.name === "workspace" || event.name === "focusedmon" || event.name === "movewindow" || event.name === "changefloatingmode") windowPoll.restart(); } enabled: Prefs.widgetHideFullscreen target: Hyprland } Timer { id: stowEnd onTriggered: deck.stowing = false } Timer { id: windowPoll interval: 120 onTriggered: Hyprland.refreshToplevels() } Item { id: deck // bumped whenever the repeater's children change, so frameAt() bindings re-run property int rev: 0 property Item dragFrame: null readonly property string screenName: layer.screen ? layer.screen.name : "" readonly property bool isPrimary: layer.screen !== null && layer.screen === Monitors.mainScreen // the widget panel is open on this screen, and still on its way out readonly property bool panelUp: Widgets.panelScreen !== "" && Widgets.panelScreen === deck.screenName readonly property bool panelLive: deck.panelUp || panel.shown // the cards are out: on, or the panel is up to place them readonly property bool widgetsOn: Prefs.widgetsEnabled || deck.panelLive // put away a moment ago: the board stays up while the cards go property bool stowing: false readonly property int stowStep: Math.round(30 * Theme.motionScale) readonly property int wakeStep: Math.round(55 * Theme.motionScale) // the order the cards go and come back in: down the screen, then across function orderOf(f) { var n = 0; for (var i = 0; i < rep.count; i++) { var o = rep.itemAt(i); if (o && o !== f && o.onThisScreen && (o.y < f.y - 1 || (Math.abs(o.y - f.y) <= 1 && o.x < f.x))) n++; } return n; } onWidgetsOnChanged: { if (deck.widgetsOn) { stowEnd.stop(); deck.stowing = false; } else { stowEnd.interval = rep.count * deck.stowStep + Theme.durMedium + 80; deck.stowing = true; stowEnd.restart(); } } readonly property int frameCount: rep.count readonly property bool editing: Widgets.editUid !== "" && deck.editFrame !== null // only ask for keys at all once something you can type into is placed; // on-demand hands focus back the moment a window is clicked, which // exclusive would not readonly property bool wantsKeyboard: { for (var i = 0; i < Widgets.model.count; i++) { var e = Widgets.model.get(i); if (!e.closing && (e.wtype === "notes" || e.wtype === "todo" || e.wtype === "github")) return true; } return false; } readonly property bool grabbing: Widgets.dragUid !== "" || Widgets.menuUid !== "" || deck.editing readonly property Item menuFrame: { var _ = deck.rev; for (var i = 0; i < rep.count; i++) { var f = rep.itemAt(i); if (f && f.uid === Widgets.menuUid && f.visible) return f; } return null; } readonly property Item editFrame: { var _ = deck.rev; for (var i = 0; i < rep.count; i++) { var f = rep.itemAt(i); if (f && f.uid === Widgets.editUid && f.visible) return f; } return null; } function frameAt(i) { var _ = deck.rev; return (i >= 0 && i < rep.count) ? rep.itemAt(i) : null; } function frameFor(uid) { for (var i = 0; i < rep.count; i++) { var f = rep.itemAt(i); if (f && f.uid === uid) return f; } return null; } // a board point over the open panel, where a dragged card is let go of function binHit(px, py) { return deck.panelUp && panel.sheetHit(px, py); } // the topmost card under a board point, for the menu overlay: it // covers the screen while it is up, so the cards never see the press function frameUnder(px, py) { var best = null; for (var i = 0; i < rep.count; i++) { var f = rep.itemAt(i); if (!f || !f.visible || f.closing) continue; if (px < f.x || px > f.x + f.width || py < f.y || py > f.y + f.height) continue; if (best === null || f.z >= best.z) best = f; } return best; } anchors.fill: parent focus: true Keys.onEscapePressed: { var busy = Widgets.editUid !== "" || Widgets.menuUid !== "" || Object.keys(Widgets.selection).length > 0; Widgets.editUid = ""; Widgets.menuUid = ""; Widgets.clearSelection(); if (!busy && deck.panelUp) Widgets.closePanel(); } onWidthChanged: deck.report() onHeightChanged: deck.report() Component.onCompleted: deck.report() function report() { if (deck.isPrimary && deck.width > 0) { Widgets.canvasW = deck.width; Widgets.canvasH = deck.height; } } // the dim behind the cards while the widget panel is open, and the way out of it Rectangle { anchors.fill: parent color: Theme.alpha(Theme.cScrim, Theme.blurAmount > 0 ? 0.3 : 0.5) opacity: panel.reveal visible: opacity > 0.01 WheelHandler { acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad enabled: deck.panelUp onWheel: (event) => { return event.accepted = true; } } } // click-through everywhere except the cards, so this only fires for the // open menu, a note being typed in, or the widget panel MouseArea { anchors.fill: parent enabled: deck.grabbing || deck.panelUp acceptedButtons: Qt.LeftButton | Qt.RightButton onPressed: { var busy = Widgets.menuUid !== "" || Widgets.editUid !== ""; Widgets.menuUid = ""; Widgets.editUid = ""; if (!busy && deck.panelUp) { Widgets.clearSelection(); Widgets.closePanel(); } } } // the cards stack among themselves, under the guides and the panel Item { anchors.fill: parent Repeater { id: rep model: Widgets.model onItemAdded: deck.rev++ onItemRemoved: deck.rev++ WidgetFrame { board: deck updatesEnabled: layer.visible } } } Rectangle { width: 1 height: deck.height x: deck.dragFrame ? deck.dragFrame.guideX : -1 color: Theme.accent opacity: (deck.dragFrame && deck.dragFrame.guideX >= 0) ? 0.65 : 0 visible: opacity > 0.01 z: 900 Behavior on opacity { NumberAnimation { duration: Theme.durQuick } } } Rectangle { height: 1 width: deck.width y: deck.dragFrame ? deck.dragFrame.guideY : -1 color: Theme.accent opacity: (deck.dragFrame && deck.dragFrame.guideY >= 0) ? 0.65 : 0 visible: opacity > 0.01 z: 900 Behavior on opacity { NumberAnimation { duration: Theme.durQuick } } } WidgetPanel { id: panel anchors.fill: parent z: 1000 board: deck } } mask: Region { x: 0 y: 0 width: (deck.grabbing || deck.panelUp) ? layer.width : 0 height: (deck.grabbing || deck.panelUp) ? layer.height : 0 WidgetRegion { frame: deck.frameAt(0) } WidgetRegion { frame: deck.frameAt(1) } WidgetRegion { frame: deck.frameAt(2) } WidgetRegion { frame: deck.frameAt(3) } WidgetRegion { frame: deck.frameAt(4) } WidgetRegion { frame: deck.frameAt(5) } WidgetRegion { frame: deck.frameAt(6) } WidgetRegion { frame: deck.frameAt(7) } WidgetRegion { frame: deck.frameAt(8) } WidgetRegion { frame: deck.frameAt(9) } WidgetRegion { frame: deck.frameAt(10) } WidgetRegion { frame: deck.frameAt(11) } WidgetRegion { frame: deck.frameAt(12) } WidgetRegion { frame: deck.frameAt(13) } WidgetRegion { frame: deck.frameAt(14) } WidgetRegion { frame: deck.frameAt(15) } WidgetRegion { frame: deck.frameAt(16) } WidgetRegion { frame: deck.frameAt(17) } WidgetRegion { frame: deck.frameAt(18) } WidgetRegion { frame: deck.frameAt(19) } } // the open panel frosts the whole screen behind its dim, cards and sheet alike BackgroundEffect.blurRegion: Theme.blurAmount > 0 ? (deck.panelUp ? wholeBlur : widgetBlur) : null Region { id: wholeBlur width: layer.width height: layer.height } Region { id: widgetBlur WidgetRegion { blur: true frame: deck.frameAt(0) } WidgetRegion { blur: true frame: deck.frameAt(1) } WidgetRegion { blur: true frame: deck.frameAt(2) } WidgetRegion { blur: true frame: deck.frameAt(3) } WidgetRegion { blur: true frame: deck.frameAt(4) } WidgetRegion { blur: true frame: deck.frameAt(5) } WidgetRegion { blur: true frame: deck.frameAt(6) } WidgetRegion { blur: true frame: deck.frameAt(7) } WidgetRegion { blur: true frame: deck.frameAt(8) } WidgetRegion { blur: true frame: deck.frameAt(9) } WidgetRegion { blur: true frame: deck.frameAt(10) } WidgetRegion { blur: true frame: deck.frameAt(11) } WidgetRegion { blur: true frame: deck.frameAt(12) } WidgetRegion { blur: true frame: deck.frameAt(13) } WidgetRegion { blur: true frame: deck.frameAt(14) } WidgetRegion { blur: true frame: deck.frameAt(15) } WidgetRegion { blur: true frame: deck.frameAt(16) } WidgetRegion { blur: true frame: deck.frameAt(17) } WidgetRegion { blur: true frame: deck.frameAt(18) } WidgetRegion { blur: true frame: deck.frameAt(19) } } } // an options panel on the widget layer would sit under every window, so it // gets an overlay of its own that only exists while the panel is up PanelWindow { id: menuLayer // the frame the panel belongs to, held past the close so the fade-out // keeps its geometry and its contents property Item held: null readonly property Item target: deck.menuFrame screen: unit.modelData // up the moment the menu opens, so the fade-in plays inside a mapped // surface, and stays up until the fade-out finishes visible: menuLayer.target !== null || menu.visible color: "transparent" exclusionMode: ExclusionMode.Ignore WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.keyboardFocus: menuLayer.target !== null ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None onTargetChanged: if (menuLayer.target) menuLayer.held = menuLayer.target onVisibleChanged: if (!menuLayer.visible) menuLayer.held = null anchors { top: true bottom: true left: true right: true } Item { anchors.fill: parent focus: true Keys.onEscapePressed: Widgets.menuUid = "" MouseArea { anchors.fill: parent acceptedButtons: Qt.LeftButton | Qt.RightButton onPressed: (mouse) => { var f = mouse.button === Qt.RightButton ? deck.frameUnder(mouse.x, mouse.y) : null; var was = Widgets.menuUid; Widgets.menuUid = ""; // a right press on another card carries the panel over to it, // the way it did when the panel still sat on the widget layer if (f && f.uid !== was) f.toggleMenu(mouse.x - f.x, mouse.y - f.y); } } WidgetMenu { id: menu frame: menuLayer.held fieldW: menuLayer.width fieldH: menuLayer.height } } // no input once the panel starts closing, so the fade-out is never a // dead region over the screen mask: Region { width: menuLayer.target ? menuLayer.width : 0 height: menuLayer.target ? menuLayer.height : 0 } BackgroundEffect.blurRegion: Theme.blurAmount > 0 ? menuBlur : null Region { id: menuBlur WidgetRegion { blur: true part: "menu" panel: menu } } } } }