Files
nixos-config/modules/user/lucid/WidgetLayer.qml
T

618 lines
20 KiB
QML

import QtQuick
import Quickshell
import Quickshell.Hyprland
import Quickshell.Wayland
import qs
Variants {
id: variants
model: Quickshell.screens
Scope {
id: unit
required property var modelData
PanelWindow {
id: layer
PaletteFade {
active: layer.visible
}
readonly property bool suppressed: Prefs.widgetHideFullscreen && layer.windowUp
readonly property var workspaceMonitor: Hyprland.monitorFor(layer.screen)
readonly property var activeWorkspace: workspaceMonitor ? workspaceMonitor.activeWorkspace : null
readonly property bool windowUp: {
const workspace = layer.activeWorkspace;
if (!workspace)
return false;
return Hyprland.toplevels.values.some((t) => {
const o = t.lastIpcObject || {};
if (!o.workspace || o.workspace.id !== workspace.id || o.mapped === false || o.hidden === true)
return false;
if (o.size && (o.size[0] <= 0 || o.size[1] <= 0))
return false;
return (o.fullscreen || 0) > 0 || o.floating === false;
});
}
screen: unit.modelData
visible: Prefs.loaded && Widgets.loaded && (deck.panelLive || ((Prefs.widgetsEnabled || deck.stowing) && Widgets.count > 0 && !layer.suppressed))
color: "transparent"
// reserves nothing and refuses to be shrunk into the bar and dock's strips,
// so a card can be dragged to a true screen edge and sit under the dock.
// setting exclusiveZone at all would silently undo this
exclusionMode: ExclusionMode.Ignore
// the widget panel lifts the whole board over the windows until it has slid away
WlrLayershell.layer: deck.panelLive ? WlrLayer.Overlay : (Prefs.widgetOnTop ? WlrLayer.Top : WlrLayer.Bottom)
WlrLayershell.keyboardFocus: deck.panelUp ? WlrKeyboardFocus.Exclusive : (deck.wantsKeyboard ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None)
anchors {
top: true
bottom: true
left: true
right: true
}
// lastIpcObject only moves when something asks it to, so nudge it on
// the events that can change whether a tiled or fullscreen window exists.
Connections {
function onRawEvent(event) {
if (event.name === "fullscreen" || event.name === "activewindowv2" || event.name === "closewindow" || event.name === "openwindow" || event.name === "workspace" || event.name === "focusedmon" || event.name === "movewindow" || event.name === "changefloatingmode")
windowPoll.restart();
}
enabled: Prefs.widgetHideFullscreen
target: Hyprland
}
Timer {
id: stowEnd
onTriggered: deck.stowing = false
}
Timer {
id: windowPoll
interval: 120
onTriggered: Hyprland.refreshToplevels()
}
Item {
id: deck
// bumped whenever the repeater's children change, so frameAt() bindings re-run
property int rev: 0
property Item dragFrame: null
readonly property string screenName: layer.screen ? layer.screen.name : ""
readonly property bool isPrimary: layer.screen !== null && layer.screen === Monitors.mainScreen
// the widget panel is open on this screen, and still on its way out
readonly property bool panelUp: Widgets.panelScreen !== "" && Widgets.panelScreen === deck.screenName
readonly property bool panelLive: deck.panelUp || panel.shown
// the cards are out: on, or the panel is up to place them
readonly property bool widgetsOn: Prefs.widgetsEnabled || deck.panelLive
// put away a moment ago: the board stays up while the cards go
property bool stowing: false
readonly property int stowStep: Math.round(30 * Theme.motionScale)
readonly property int wakeStep: Math.round(55 * Theme.motionScale)
// the order the cards go and come back in: down the screen, then across
function orderOf(f) {
var n = 0;
for (var i = 0; i < rep.count; i++) {
var o = rep.itemAt(i);
if (o && o !== f && o.onThisScreen && (o.y < f.y - 1 || (Math.abs(o.y - f.y) <= 1 && o.x < f.x)))
n++;
}
return n;
}
onWidgetsOnChanged: {
if (deck.widgetsOn) {
stowEnd.stop();
deck.stowing = false;
} else {
stowEnd.interval = rep.count * deck.stowStep + Theme.durMedium + 80;
deck.stowing = true;
stowEnd.restart();
}
}
readonly property int frameCount: rep.count
readonly property bool editing: Widgets.editUid !== "" && deck.editFrame !== null
// only ask for keys at all once something you can type into is placed;
// on-demand hands focus back the moment a window is clicked, which
// exclusive would not
readonly property bool wantsKeyboard: {
for (var i = 0; i < Widgets.model.count; i++) {
var e = Widgets.model.get(i);
if (!e.closing && (e.wtype === "notes" || e.wtype === "todo" || e.wtype === "github"))
return true;
}
return false;
}
readonly property bool grabbing: Widgets.dragUid !== "" || Widgets.menuUid !== "" || deck.editing
readonly property Item menuFrame: {
var _ = deck.rev;
for (var i = 0; i < rep.count; i++) {
var f = rep.itemAt(i);
if (f && f.uid === Widgets.menuUid && f.visible)
return f;
}
return null;
}
readonly property Item editFrame: {
var _ = deck.rev;
for (var i = 0; i < rep.count; i++) {
var f = rep.itemAt(i);
if (f && f.uid === Widgets.editUid && f.visible)
return f;
}
return null;
}
function frameAt(i) {
var _ = deck.rev;
return (i >= 0 && i < rep.count) ? rep.itemAt(i) : null;
}
function frameFor(uid) {
for (var i = 0; i < rep.count; i++) {
var f = rep.itemAt(i);
if (f && f.uid === uid)
return f;
}
return null;
}
// a board point over the open panel, where a dragged card is let go of
function binHit(px, py) {
return deck.panelUp && panel.sheetHit(px, py);
}
// the topmost card under a board point, for the menu overlay: it
// covers the screen while it is up, so the cards never see the press
function frameUnder(px, py) {
var best = null;
for (var i = 0; i < rep.count; i++) {
var f = rep.itemAt(i);
if (!f || !f.visible || f.closing)
continue;
if (px < f.x || px > f.x + f.width || py < f.y || py > f.y + f.height)
continue;
if (best === null || f.z >= best.z)
best = f;
}
return best;
}
anchors.fill: parent
focus: true
Keys.onEscapePressed: {
var busy = Widgets.editUid !== "" || Widgets.menuUid !== "" || Object.keys(Widgets.selection).length > 0;
Widgets.editUid = "";
Widgets.menuUid = "";
Widgets.clearSelection();
if (!busy && deck.panelUp)
Widgets.closePanel();
}
onWidthChanged: deck.report()
onHeightChanged: deck.report()
Component.onCompleted: deck.report()
function report() {
if (deck.isPrimary && deck.width > 0) {
Widgets.canvasW = deck.width;
Widgets.canvasH = deck.height;
}
}
// the dim behind the cards while the widget panel is open, and the way out of it
Rectangle {
anchors.fill: parent
color: Theme.alpha(Theme.cScrim, Theme.blurAmount > 0 ? 0.3 : 0.5)
opacity: panel.reveal
visible: opacity > 0.01
WheelHandler {
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
enabled: deck.panelUp
onWheel: (event) => {
return event.accepted = true;
}
}
}
// click-through everywhere except the cards, so this only fires for the
// open menu, a note being typed in, or the widget panel
MouseArea {
anchors.fill: parent
enabled: deck.grabbing || deck.panelUp
acceptedButtons: Qt.LeftButton | Qt.RightButton
onPressed: {
var busy = Widgets.menuUid !== "" || Widgets.editUid !== "";
Widgets.menuUid = "";
Widgets.editUid = "";
if (!busy && deck.panelUp) {
Widgets.clearSelection();
Widgets.closePanel();
}
}
}
// the cards stack among themselves, under the guides and the panel
Item {
anchors.fill: parent
Repeater {
id: rep
model: Widgets.model
onItemAdded: deck.rev++
onItemRemoved: deck.rev++
WidgetFrame {
board: deck
updatesEnabled: layer.visible
}
}
}
Rectangle {
width: 1
height: deck.height
x: deck.dragFrame ? deck.dragFrame.guideX : -1
color: Theme.accent
opacity: (deck.dragFrame && deck.dragFrame.guideX >= 0) ? 0.65 : 0
visible: opacity > 0.01
z: 900
Behavior on opacity {
NumberAnimation {
duration: Theme.durQuick
}
}
}
Rectangle {
height: 1
width: deck.width
y: deck.dragFrame ? deck.dragFrame.guideY : -1
color: Theme.accent
opacity: (deck.dragFrame && deck.dragFrame.guideY >= 0) ? 0.65 : 0
visible: opacity > 0.01
z: 900
Behavior on opacity {
NumberAnimation {
duration: Theme.durQuick
}
}
}
WidgetPanel {
id: panel
anchors.fill: parent
z: 1000
board: deck
}
}
mask: Region {
x: 0
y: 0
width: (deck.grabbing || deck.panelUp) ? layer.width : 0
height: (deck.grabbing || deck.panelUp) ? layer.height : 0
WidgetRegion {
frame: deck.frameAt(0)
}
WidgetRegion {
frame: deck.frameAt(1)
}
WidgetRegion {
frame: deck.frameAt(2)
}
WidgetRegion {
frame: deck.frameAt(3)
}
WidgetRegion {
frame: deck.frameAt(4)
}
WidgetRegion {
frame: deck.frameAt(5)
}
WidgetRegion {
frame: deck.frameAt(6)
}
WidgetRegion {
frame: deck.frameAt(7)
}
WidgetRegion {
frame: deck.frameAt(8)
}
WidgetRegion {
frame: deck.frameAt(9)
}
WidgetRegion {
frame: deck.frameAt(10)
}
WidgetRegion {
frame: deck.frameAt(11)
}
WidgetRegion {
frame: deck.frameAt(12)
}
WidgetRegion {
frame: deck.frameAt(13)
}
WidgetRegion {
frame: deck.frameAt(14)
}
WidgetRegion {
frame: deck.frameAt(15)
}
WidgetRegion {
frame: deck.frameAt(16)
}
WidgetRegion {
frame: deck.frameAt(17)
}
WidgetRegion {
frame: deck.frameAt(18)
}
WidgetRegion {
frame: deck.frameAt(19)
}
}
// the open panel frosts the whole screen behind its dim, cards and sheet alike
BackgroundEffect.blurRegion: Theme.blurAmount > 0 ? (deck.panelUp ? wholeBlur : widgetBlur) : null
Region {
id: wholeBlur
width: layer.width
height: layer.height
}
Region {
id: widgetBlur
WidgetRegion {
blur: true
frame: deck.frameAt(0)
}
WidgetRegion {
blur: true
frame: deck.frameAt(1)
}
WidgetRegion {
blur: true
frame: deck.frameAt(2)
}
WidgetRegion {
blur: true
frame: deck.frameAt(3)
}
WidgetRegion {
blur: true
frame: deck.frameAt(4)
}
WidgetRegion {
blur: true
frame: deck.frameAt(5)
}
WidgetRegion {
blur: true
frame: deck.frameAt(6)
}
WidgetRegion {
blur: true
frame: deck.frameAt(7)
}
WidgetRegion {
blur: true
frame: deck.frameAt(8)
}
WidgetRegion {
blur: true
frame: deck.frameAt(9)
}
WidgetRegion {
blur: true
frame: deck.frameAt(10)
}
WidgetRegion {
blur: true
frame: deck.frameAt(11)
}
WidgetRegion {
blur: true
frame: deck.frameAt(12)
}
WidgetRegion {
blur: true
frame: deck.frameAt(13)
}
WidgetRegion {
blur: true
frame: deck.frameAt(14)
}
WidgetRegion {
blur: true
frame: deck.frameAt(15)
}
WidgetRegion {
blur: true
frame: deck.frameAt(16)
}
WidgetRegion {
blur: true
frame: deck.frameAt(17)
}
WidgetRegion {
blur: true
frame: deck.frameAt(18)
}
WidgetRegion {
blur: true
frame: deck.frameAt(19)
}
}
}
// an options panel on the widget layer would sit under every window, so it
// gets an overlay of its own that only exists while the panel is up
PanelWindow {
id: menuLayer
// the frame the panel belongs to, held past the close so the fade-out
// keeps its geometry and its contents
property Item held: null
readonly property Item target: deck.menuFrame
screen: unit.modelData
// up the moment the menu opens, so the fade-in plays inside a mapped
// surface, and stays up until the fade-out finishes
visible: menuLayer.target !== null || menu.visible
color: "transparent"
exclusionMode: ExclusionMode.Ignore
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: menuLayer.target !== null ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
onTargetChanged: if (menuLayer.target)
menuLayer.held = menuLayer.target
onVisibleChanged: if (!menuLayer.visible)
menuLayer.held = null
anchors {
top: true
bottom: true
left: true
right: true
}
Item {
anchors.fill: parent
focus: true
Keys.onEscapePressed: Widgets.menuUid = ""
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
onPressed: (mouse) => {
var f = mouse.button === Qt.RightButton ? deck.frameUnder(mouse.x, mouse.y) : null;
var was = Widgets.menuUid;
Widgets.menuUid = "";
// a right press on another card carries the panel over to it,
// the way it did when the panel still sat on the widget layer
if (f && f.uid !== was)
f.toggleMenu(mouse.x - f.x, mouse.y - f.y);
}
}
WidgetMenu {
id: menu
frame: menuLayer.held
fieldW: menuLayer.width
fieldH: menuLayer.height
}
}
// no input once the panel starts closing, so the fade-out is never a
// dead region over the screen
mask: Region {
width: menuLayer.target ? menuLayer.width : 0
height: menuLayer.target ? menuLayer.height : 0
}
BackgroundEffect.blurRegion: Theme.blurAmount > 0 ? menuBlur : null
Region {
id: menuBlur
WidgetRegion {
blur: true
part: "menu"
panel: menu
}
}
}
}
}