aboutsummaryrefslogtreecommitdiffstats
path: root/window-switcher/HotCorner.qml
blob: cb7d863b82783bd6ad69dd7c5b16c154007500f8 (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
// 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
    }
}