blob: 4031c3c7a35ab9878459dbacae9b3081f1bbcf79 (
plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
|
// 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: "keystone-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 glow centred on the corner, growing with the dwell. The disc is
// smaller than the window so the fade fits inside it.
Glow {
readonly property real r: corner.width * 0.5 * corner.progress
x: corner.width - r
y: -r
width: 2 * r
height: 2 * r
radius: r
blur: r
on: true
opacity: 0.8 * corner.progress
}
}
|