diff options
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 } } + } + } + } + } +} |
