diff options
| author | Danilo M. <danix@danix.xyz> | 2026-09-14 12:14:56 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-09-14 12:14:56 +0200 |
| commit | 14a8cd8256620c151693cf2098a91403aa76c3cb (patch) | |
| tree | 3baeb919c61f8205e2f48e9ccf3b91ffbc9e67e5 /desktop/Drawer.qml | |
| parent | 26277c179016b86b95d9f8185dab9dfed4474fc6 (diff) | |
| download | quickshell-14a8cd8256620c151693cf2098a91403aa76c3cb.tar.gz quickshell-14a8cd8256620c151693cf2098a91403aa76c3cb.zip | |
feat(desktop): the drawer, with the top reserved
Left of DP-1 because conky holds the right; ExclusionMode.Normal so the
bar the drawer is reached from stays visible and clickable. The top is an
empty Item claiming the space the notification engine will fill, so the
grid already sits where it will sit once that lands.
Diffstat (limited to 'desktop/Drawer.qml')
| -rw-r--r-- | desktop/Drawer.qml | 187 |
1 files changed, 187 insertions, 0 deletions
diff --git a/desktop/Drawer.qml b/desktop/Drawer.qml new file mode 100644 index 0000000..f0e0a84 --- /dev/null +++ b/desktop/Drawer.qml @@ -0,0 +1,187 @@ +// Copyright (C) 2026 Danilo M. <danix@danix.xyz> +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +import Quickshell +import Quickshell.Wayland +import QtQuick + +Scope { + id: root + + // The modules the drawer hosts, in grid order. Set from shell.qml. + property list<QtObject> modules + + // Waybar runs on this screen and the launcher sits at its left end, so + // the drawer belongs here. Falls back to the first screen when this + // monitor is not connected, so the drawer is never invisible. + property string monitor: "DP-1" + + property bool open: false + + // Which module's page is showing. Empty means the grid. + property string page: "" + + readonly property var screenObj: + Quickshell.screens.find(s => s.name === root.monitor) ?? Quickshell.screens[0] + + // A plain `list<QtObject>` is indexed directly; it is not an + // ObjectModel, so there is no `.values` to go through. + readonly property QtObject current: { + for (let i = 0; i < root.modules.length; i++) + if (root.modules[i].name === root.page) return root.modules[i]; + return null; + } + + function show(name) { + root.page = name ?? ""; + root.open = true; + } + + function close() { + root.open = false; + // Reset to the grid: a panel that reopens somewhere unexpected is + // worse than one extra click. + root.page = ""; + } + + function toggle(name) { + if (root.open && (name ?? "") === root.page) root.close(); + else root.show(name); + } + + // Clicking a tile: a module with a page opens it, one without acts. + function activate(mod) { + if (mod.page) root.page = mod.name; + else { mod.activate(); root.close(); } + } + + LazyLoader { + active: root.open + + PanelWindow { + id: win + screen: root.screenObj + + anchors { top: true; left: true; right: true; bottom: true } + color: "transparent" + + // Normal, not Ignore: waybar claims an exclusive zone at the top + // of this screen, so respecting it puts the drawer below the bar + // without this file knowing the bar's height. The drawer is + // reached from the bar, so the bar must stay visible and + // clickable while it is open. + exclusionMode: ExclusionMode.Normal + + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.namespace: "quickshell-desktop" + WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive + + // The click-outside catcher. It covers the whole surface, and the + // drawer sits on top of it swallowing its own clicks. + MouseArea { + anchors.fill: parent + onClicked: root.close() + } + + // Keys reach a focused item, never the window: setting + // keyboardFocus above is necessary but not sufficient, and + // Keys.onEscapePressed on a PanelWindow never fires. See AGENTS.md. + Item { + anchors.fill: parent + focus: true + Keys.onEscapePressed: { + if (root.page) root.page = ""; + else root.close(); + } + } + + Rectangle { + id: panel + anchors { top: parent.top; left: parent.left; bottom: parent.bottom } + width: 600 + color: Qt.alpha(Theme.base, 0.72) + topRightRadius: 14 + bottomRightRadius: 14 + border.width: 1 + border.color: Qt.alpha(Theme.text, 0.12) + + // Clicks on the panel must not reach the catcher behind it. + MouseArea { anchors.fill: parent } + + // --- grid view --- + + Item { + anchors.fill: parent + anchors.margins: 16 + visible: root.page === "" + + // Reserved for the notification engine. An empty Item that + // claims the space rather than a placeholder graphic: the + // grid has to sit where it will sit once notifications + // arrive, or the layout is tuned against a position that + // does not survive. + Item { + id: notifications + anchors { top: parent.top; left: parent.left; right: parent.right } + anchors.bottom: grid.top + anchors.bottomMargin: 16 + } + + // Fixed, never scrolled. Three columns at 600px with a + // 180px minimum; tiles wrap and add rows, ceiling 3x3. + Flow { + id: grid + anchors { left: parent.left; right: parent.right; bottom: parent.bottom } + spacing: 10 + + Repeater { + model: root.modules + + Tile { + required property QtObject modelData + // Three columns, or fewer if the panel is + // narrower than three 180px tiles allow. + width: (grid.width - 2 * grid.spacing) / 3 + icon: modelData.icon + label: modelData.label + content: modelData.tileContent + onClicked: root.activate(modelData) + } + } + } + } + + // --- page view --- + + Loader { + id: pageLoader + anchors.fill: parent + anchors.margins: 16 + active: root.current !== null + sourceComponent: root.current?.page ?? null + + // The Page's back arrow returns to the grid. Without this + // the header button is inert, since the page only emits + // `back` and does not know who hosts it. + onLoaded: if (item && item.back) item.back.connect(() => root.page = "") + + // The page enters from the right: the one piece of motion + // in the design, and what makes the drawer read as one + // surface rather than a window swapping contents. + opacity: active ? 1 : 0 + x: active ? 16 : 60 + Behavior on x { NumberAnimation { duration: 160; easing.type: Easing.OutCubic } } + Behavior on opacity { NumberAnimation { duration: 160 } } + } + } + } + } +} |
