diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-15 11:03:49 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-15 11:03:49 +0200 |
| commit | 00a3eb4047b76840233bb9ee872f20b7280ba427 (patch) | |
| tree | 339acfe1e60890ac4d7582c29db4d590f71bb598 /window-switcher/HotCorner.qml | |
| parent | 4cc85e59d7b3f53f4eec897f022bc4bcb643f185 (diff) | |
| download | quickshell-00a3eb4047b76840233bb9ee872f20b7280ba427.tar.gz quickshell-00a3eb4047b76840233bb9ee872f20b7280ba427.zip | |
feat(window-switcher): the expose replaces the grid, with a hot corner
Previews are placed at their Expose.js rects and sized to them, so each
keeps its window's shape and relative size. Title and close show on hover
only, so the scatter stays clean; icon and workspace marker always show.
The keyboard selection is gone: this is a pointer feature, and Escape is
the one key left.
The top-right corner of DP-1 opens it after a 250ms glow that grows out of
the corner, which is the warning and the way out. Input is only the corner's
2x2 pixels, since a larger input region would eat clicks on the windows
below. It re-arms only when the pointer leaves while the expose is closed,
or an Escape with the pointer still in the corner reopens it. It hides while
status.gaming is set, read straight from the file: linking shared/Status.qml
would start a second game poll and fire presentation side effects twice.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'window-switcher/HotCorner.qml')
| -rw-r--r-- | window-switcher/HotCorner.qml | 101 |
1 files changed, 101 insertions, 0 deletions
diff --git a/window-switcher/HotCorner.qml b/window-switcher/HotCorner.qml new file mode 100644 index 0000000..cb7d863 --- /dev/null +++ b/window-switcher/HotCorner.qml @@ -0,0 +1,101 @@ +// 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.Io +import Quickshell.Wayland +import QtQuick + +// The top-right corner of DP-1 opens the expose. Touching it grows a glow out +// of the corner over 250ms and the expose opens when the glow is full; +// leaving first shrinks it back and nothing happens. The dwell is what keeps +// a pointer passing through from triggering it, and the glow is the warning. +PanelWindow { + id: corner + + // True while the expose is up. Set by shell.qml. + property bool blocked: false + + signal triggered() + + screen: Quickshell.screens.find(s => s.name === "DP-1") ?? Quickshell.screens[0] + anchors { top: true; right: true } + implicitWidth: 48 + implicitHeight: 48 + color: "transparent" + exclusionMode: ExclusionMode.Ignore + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.namespace: "quickshell-hot-corner" + WlrLayershell.keyboardFocus: WlrKeyboardFocus.None + + // Off while a game runs. Read straight from the status registry's file: + // linking shared/Status.qml instead would start a second copy of its game + // poll and its presentation side effects in this process. + visible: !gaming + property bool gaming: false + + FileView { + id: gamingFile + path: (Quickshell.env("XDG_RUNTIME_DIR") || "/tmp") + "/status.gaming" + watchChanges: true + printErrors: false + onFileChanged: reload() + onLoaded: corner.gaming = text().trim() === "1" + // A missing file is the off state. + onLoadFailed: corner.gaming = false + } + + // Input only on the corner's 2x2 pixels. The rest of the 48x48 is the + // glow, drawn but click-through: a larger input area would swallow clicks + // meant for the windows underneath, which is also why the glow cannot + // start before the pointer is in the corner. + mask: Region { item: patch } + + Item { + id: patch + anchors { top: parent.top; right: parent.right } + width: 2 + height: 2 + + HoverHandler { + id: hover + // Re-armed only by leaving while the expose is closed. Opening it + // covers the corner, which reads as leaving; closing it with + // Escape while the pointer still sits here would otherwise reopen + // it 250ms later. + onHoveredChanged: if (!hovered && !corner.blocked) corner.armed = true + } + } + + property bool armed: true + + property real progress: hover.hovered && armed && !blocked ? 1 : 0 + Behavior on progress { NumberAnimation { duration: 250 } } + + onProgressChanged: { + if (progress >= 1) { + armed = false; + triggered(); + } + } + + // A quarter circle centred on the corner, growing with the dwell. + Rectangle { + readonly property real r: corner.width * corner.progress + x: corner.width - r + y: -r + width: 2 * r + height: 2 * r + radius: r + color: Theme.accent + opacity: 0.6 * corner.progress + } +} |
