diff options
| -rw-r--r-- | desktop/modules/appearance/WallEngine.qml | 115 |
1 files changed, 115 insertions, 0 deletions
diff --git a/desktop/modules/appearance/WallEngine.qml b/desktop/modules/appearance/WallEngine.qml new file mode 100644 index 0000000..9d90245 --- /dev/null +++ b/desktop/modules/appearance/WallEngine.qml @@ -0,0 +1,115 @@ +// 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. + +pragma Singleton + +import Quickshell +import QtQuick +import "../.." +import "Shuffle.js" as Shuffle + +// The wallpaper engine: both wallpapers on a timer, from a list per screen +// picked with the heart in the picker. Ticks go through Wallpapers.run like +// Apply, so the burn plays, but as the light run: udt-accent leaves GRUB, the +// homepage and SDDM alone. Each screen has a home, the wallpaper last chosen +// for it by hand, and switching off goes back to it with a full run, so those +// three match the desktop again. See the spec, +// docs/superpowers/specs/2026-10-29-wallpaper-engine-design.md. +Singleton { + id: root + + readonly property var cfg: Prefs.wallEngine + readonly property bool on: cfg.on + + function list(which) { return which === "V" ? cfg.v : cfg.h; } + function listed(path, which) { return list(which).indexOf(path) >= 0; } + // ponytail: present means in the gallery's last scan, so a file deleted + // since stays drawable until a rescan, and wallp then skips it. + function present(path) { return Wallpapers.files.indexOf(path) >= 0; } + + function toggle(path, which) { + const l = list(which).slice(); + const i = l.indexOf(path); + if (i >= 0) l.splice(i, 1); + else l.push(path); + Prefs.setWallEngine(which === "V" ? { v: l } : { h: l }); + } + + function setOn(v) { + if (v === on) return; + if (v) { + Prefs.setWallEngine({ on: true, homeH: Wallpapers.currentH, homeV: Wallpapers.currentV }); + } else { + Prefs.setWallEngine({ on: false }); + goHome(); + } + } + + function setMinutes(m) { + if (Number.isInteger(m) && m >= 1 && m !== cfg.minutes) Prefs.setWallEngine({ minutes: m }); + } + + // ponytail: in memory, so a restart starts a new round. + property var bagH: [] + property var bagV: [] + + // A screen whose list is empty, or whose pick is already up, is left out. + function tick() { + if (Wallpapers.applying) return; + const h = Shuffle.draw(cfg.h.filter(present), bagH, Wallpapers.currentH); + const v = Shuffle.draw(cfg.v.filter(present), bagV, Wallpapers.currentV); + const ph = h.pick !== Wallpapers.currentH ? h.pick : ""; + const pv = v.pick !== Wallpapers.currentV ? v.pick : ""; + if (Wallpapers.run(ph, pv, "tick")) { + bagH = h.bag; + bagV = v.bag; + } + } + + function next() { + tick(); + timer.restart(); + } + + // A switch-off during a run cannot start its own; it goes when that run + // finishes. + property bool homeWaiting: false + function goHome() { + const h = cfg.homeH !== Wallpapers.currentH ? cfg.homeH : ""; + const v = cfg.homeV !== Wallpapers.currentV ? cfg.homeV : ""; + if (h === "" && v === "") return; + homeWaiting = !Wallpapers.run(h, v, "home"); + } + + Timer { + id: timer + interval: root.cfg.minutes * 60000 + repeat: true + running: root.on + onTriggered: root.tick() + } + + Connections { + target: Wallpapers + // A pick by hand is the new home, and stays up a full interval. + function onManualApplied(h, v) { + if (!root.on) return; + Prefs.setWallEngine({ homeH: h !== "" ? h : root.cfg.homeH, + homeV: v !== "" ? v : root.cfg.homeV }); + timer.restart(); + } + function onFinished() { + if (!root.homeWaiting || root.on) return; + root.homeWaiting = false; + root.goHome(); + } + } +} |
