Author SHA1 Message Date
zach 789305286a Merge branch 'main' into feat/device-pixel-alignment
C++ / fmt (pull_request) Failing after 6s
C++ / build (pull_request) Failing after 3m19s
C++ / clang-tidy (pull_request) Failing after 3m17s
JS/TS / fmt (pull_request) Successful in 10s
JS/TS / lint (pull_request) Successful in 32s
Python / static (pull_request) Successful in 28s
Rust / build (pull_request) Successful in 49s
Python / verify (pull_request) Failing after 1m20s
Rust / fmt (pull_request) Successful in 33s
Rust / clippy (pull_request) Successful in 57s
2026-11-01 12:50:58 +01:00
zach f8101b2807 feat: sql database for notifications 2026-11-01 12:43:21 +01:00
zach 017d7073d2 Merge branch 'main' into feat/device-pixel-alignment
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 3m41s
C++ / clang-tidy (pull_request) Failing after 3m39s
JS/TS / fmt (pull_request) Successful in 13s
JS/TS / lint (pull_request) Successful in 11s
Python / static (pull_request) Successful in 34s
Rust / build (pull_request) Successful in 49s
Python / verify (pull_request) Failing after 1m29s
Rust / fmt (pull_request) Successful in 42s
Rust / clippy (pull_request) Successful in 1m2s
2026-11-01 01:57:30 +01:00
zach 8271e5d013 feat: testing new kinetic scroller for notifications in sidebar 2026-11-01 01:57:13 +01:00
zach 566b71d386 feat: drawing popout now clips strokes again + add redo + add undo and redo keybinds
C++ / fmt (pull_request) Failing after 6s
C++ / build (pull_request) Failing after 12s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 9s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Successful in 28s
Python / verify (pull_request) Failing after 1m41s
Rust / build (pull_request) Successful in 1m15s
Rust / fmt (pull_request) Successful in 42s
Rust / clippy (pull_request) Successful in 1m6s
2026-10-30 14:16:19 +01:00
zach c5608dc9b9 fix: blobs now allow to be drawn in device pixels to stay sharp
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 9s
JS/TS / lint (pull_request) Successful in 25s
Python / static (pull_request) Successful in 34s
Rust / build (pull_request) Successful in 51s
Python / verify (pull_request) Failing after 1m25s
Rust / fmt (pull_request) Successful in 39s
Rust / clippy (pull_request) Successful in 59s
2026-10-30 01:52:33 +01:00
zach 78a9577209 Merge branch 'main' into feat/device-pixel-alignment
C++ / fmt (pull_request) Failing after 6s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 7s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Successful in 26s
Rust / build (pull_request) Successful in 54s
Python / verify (pull_request) Failing after 1m29s
Rust / fmt (pull_request) Successful in 36s
Rust / clippy (pull_request) Successful in 50s
2026-10-29 20:11:33 +01:00
zach 90184bff58 Merge pull request 'feat: video wallpapers' (#163) from feat/video-wallpaper into main
Reviewed-on: #163
2026-10-29 19:55:05 +01:00
zach abdde2450d fix: don't use lambda expression
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 11s
JS/TS / fmt (pull_request) Successful in 7s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Successful in 30s
Rust / build (pull_request) Successful in 50s
Python / verify (pull_request) Failing after 1m27s
Rust / fmt (pull_request) Successful in 31s
Rust / clippy (pull_request) Successful in 58s
2026-10-29 19:50:02 +01:00
zach fd05b3d4c4 Merge branch 'main' into feat/video-wallpaper
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 9s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 13s
JS/TS / lint (pull_request) Successful in 8s
Python / static (pull_request) Failing after 37s
Rust / build (pull_request) Successful in 50s
Python / verify (pull_request) Failing after 1m28s
Rust / fmt (pull_request) Successful in 39s
Rust / clippy (pull_request) Successful in 1m5s
2026-10-29 19:03:10 +01:00
zach 067291f478 fix: lockscreen notifdock
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 9s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 7s
JS/TS / lint (pull_request) Successful in 14s
Python / static (pull_request) Failing after 27s
Rust / build (pull_request) Successful in 47s
Python / verify (pull_request) Failing after 1m23s
Rust / fmt (pull_request) Successful in 35s
Rust / clippy (pull_request) Successful in 53s
2026-10-29 18:43:50 +01:00
zach 395d1dad3d fix: remove duped property 2026-10-29 17:51:20 +01:00
zach fe0ce991da feat: initial commit for pixel-snapping components and helpers 2026-10-29 17:47:19 +01:00
zach 858e4e5b12 rebase to feat/video-wallpapers 2026-10-29 17:46:27 +01:00
zach ed7e977eb5 fix: remove more uses of cliprect and finally fix workspace's layer texture 2026-10-29 17:44:19 +01:00
zach 4252812e82 fix: don't link eglDestroyImageKHR 2026-10-29 17:44:19 +01:00
zach a893e6cbe6 feat: add vaapi path for video wallpapers on generic gpus 2026-10-29 17:44:19 +01:00
zach 3792e9d73f feat: add wallpaper transitions, store crop in database per-wallpaper, move CUDA context creation to async thread 2026-10-29 17:44:19 +01:00
zach 906b15b092 fix: don't store dbPath as const 2026-10-29 17:44:19 +01:00
zach 811ad6b7f4 fix: restore code removed by incorrect replacement command 2026-10-29 17:44:19 +01:00
zach 76f5a23c2a feat: skip two-way copy for cuda devices when decoding video wallpapers + cache wallpapers as blobs in new database 2026-10-29 17:44:19 +01:00
zach c0744b29b6 chore: suppress more warnings 2026-10-29 17:44:19 +01:00
zach 5896256372 feat: initial video wallpaper implementation 2026-10-29 17:44:19 +01:00
zach 118b8f482a fix: fix sub-pixel layout arrangement for normalized resolutions
C++ / fmt (pull_request) Failing after 7s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 11s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Failing after 31s
Rust / build (pull_request) Successful in 49s
Python / verify (pull_request) Failing after 1m28s
Rust / fmt (pull_request) Successful in 30s
Rust / clippy (pull_request) Successful in 54s
2026-10-27 23:00:26 +01:00
zach b58b4fdc9c fix: remove more uses of cliprect and finally fix workspace's layer texture
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 9s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Failing after 33s
Rust / build (pull_request) Successful in 48s
Python / verify (pull_request) Failing after 1m24s
Rust / fmt (pull_request) Successful in 32s
Rust / clippy (pull_request) Successful in 57s
2026-10-27 15:15:57 +01:00
zach a56afd7fac fix: don't link eglDestroyImageKHR
C++ / fmt (pull_request) Failing after 7s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 7s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Failing after 27s
Rust / build (pull_request) Successful in 47s
Python / verify (pull_request) Failing after 1m24s
Rust / fmt (pull_request) Successful in 29s
Rust / clippy (pull_request) Successful in 45s
2026-10-27 02:30:55 +01:00
zach b0cb43f75c feat: add vaapi path for video wallpapers on generic gpus
C++ / fmt (pull_request) Failing after 6s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 11s
JS/TS / fmt (pull_request) Successful in 10s
JS/TS / lint (pull_request) Successful in 8s
Python / static (pull_request) Failing after 32s
Rust / build (pull_request) Successful in 52s
Python / verify (pull_request) Failing after 1m23s
Rust / fmt (pull_request) Successful in 51s
Rust / clippy (pull_request) Successful in 1m2s
2026-10-27 02:15:16 +01:00
zach e655ec5bc7 feat: add wallpaper transitions, store crop in database per-wallpaper, move CUDA context creation to async thread
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 9s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 10s
JS/TS / lint (pull_request) Successful in 8s
Python / static (pull_request) Failing after 28s
Python / verify (pull_request) Failing after 1m29s
Rust / build (pull_request) Successful in 53s
Rust / fmt (pull_request) Successful in 42s
Rust / clippy (pull_request) Successful in 1m4s
2026-10-26 14:47:47 +01:00
zach dc0f222ace fix: don't store dbPath as const
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 7s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Failing after 27s
Rust / build (pull_request) Successful in 47s
Python / verify (pull_request) Failing after 1m24s
Rust / fmt (pull_request) Successful in 30s
Rust / clippy (pull_request) Successful in 53s
2026-10-26 03:13:33 +01:00
zach de19fd6ac4 fix: restore code removed by incorrect replacement command
C++ / fmt (pull_request) Failing after 5s
C++ / clang-tidy (pull_request) Failing after 4s
C++ / build (pull_request) Failing after 9s
JS/TS / fmt (pull_request) Successful in 6s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Failing after 28s
Rust / build (pull_request) Successful in 48s
Python / verify (pull_request) Failing after 1m19s
Rust / fmt (pull_request) Successful in 36s
Rust / clippy (pull_request) Successful in 1m0s
2026-10-26 03:03:20 +01:00
zach 9edbcbd610 feat: skip two-way copy for cuda devices when decoding video wallpapers + cache wallpapers as blobs in new database
C++ / fmt (pull_request) Failing after 11s
C++ / build (pull_request) Failing after 14s
C++ / clang-tidy (pull_request) Failing after 9s
JS/TS / fmt (pull_request) Successful in 10s
JS/TS / lint (pull_request) Successful in 8s
Python / static (pull_request) Failing after 28s
Rust / build (pull_request) Successful in 47s
Python / verify (pull_request) Failing after 1m20s
Rust / fmt (pull_request) Successful in 28s
Rust / clippy (pull_request) Successful in 59s
2026-10-26 02:51:28 +01:00
zach cd44d27b74 chore: suppress more warnings
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 7s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Successful in 29s
Rust / build (pull_request) Successful in 59s
Python / verify (pull_request) Failing after 1m31s
Rust / fmt (pull_request) Successful in 33s
Rust / clippy (pull_request) Successful in 52s
2026-10-25 02:10:50 +02:00
zach 0d0faecb1c feat: initial video wallpaper implementation 2026-10-25 02:10:50 +02:00
131 changed files with 11717 additions and 2490 deletions
+3
View File
@@ -0,0 +1,3 @@
#omz revision: d2b7f557cceaddb16ba89bbf8200d8a8df35c4d8
#omz fpath: /home/zach/.oh-my-zsh/plugins/colored-man-pages /home/zach/.oh-my-zsh/plugins/universalarchive /home/zach/.oh-my-zsh/plugins/tldr /home/zach/.oh-my-zsh/plugins/extract /home/zach/.oh-my-zsh/plugins/copypath /home/zach/.oh-my-zsh/plugins/cp /home/zach/.oh-my-zsh/plugins/conda-env /home/zach/.oh-my-zsh/plugins/command-not-found /home/zach/.oh-my-zsh/plugins/dirhistory /home/zach/.oh-my-zsh/plugins/copybuffer /home/zach/.oh-my-zsh/plugins/copyfile /home/zach/.oh-my-zsh/custom/plugins/fast-syntax-highlighting /home/zach/.oh-my-zsh/custom/plugins/zsh-syntax-highlighting /home/zach/.oh-my-zsh/custom/plugins/zsh-autosuggestions /home/zach/.oh-my-zsh/plugins/archlinux /home/zach/.oh-my-zsh/plugins/web-search /home/zach/.oh-my-zsh/plugins/sudo /home/zach/.oh-my-zsh/plugins/git /home/zach/.oh-my-zsh/functions /home/zach/.oh-my-zsh/completions /home/zach/.oh-my-zsh/custom/functions /home/zach/.oh-my-zsh/custom/completions /home/zach/tools/ripgrep/complete /home/linuxbrew/.linuxbrew/share/zsh/site-functions /home/zach/.oh-my-zsh/plugins/colored-man-pages /home/zach/.oh-my-zsh/plugins/universalarchive /home/zach/.oh-my-zsh/plugins/tldr /home/zach/.oh-my-zsh/plugins/extract /home/zach/.oh-my-zsh/plugins/copypath /home/zach/.oh-my-zsh/plugins/cp /home/zach/.oh-my-zsh/plugins/conda-env /home/zach/.oh-my-zsh/plugins/command-not-found /home/zach/.oh-my-zsh/plugins/dirhistory /home/zach/.oh-my-zsh/plugins/copybuffer /home/zach/.oh-my-zsh/plugins/copyfile /home/zach/.oh-my-zsh/custom/plugins/fast-syntax-highlighting /home/zach/.oh-my-zsh/custom/plugins/zsh-syntax-highlighting /home/zach/.oh-my-zsh/custom/plugins/zsh-autosuggestions /home/zach/.oh-my-zsh/plugins/archlinux /home/zach/.oh-my-zsh/plugins/web-search /home/zach/.oh-my-zsh/plugins/sudo /home/zach/.oh-my-zsh/plugins/git /home/zach/.oh-my-zsh/functions /home/zach/.oh-my-zsh/completions /home/zach/.oh-my-zsh/custom/functions /home/zach/.oh-my-zsh/custom/completions /home/zach/.oh-my-zsh/cache/completions /home/zach/tools/ripgrep/complete /home/linuxbrew/.linuxbrew/share/zsh/site-functions /usr/local/share/zsh/site-functions /usr/share/zsh/site-functions /usr/share/zsh/functions/Calendar /usr/share/zsh/functions/Chpwd /usr/share/zsh/functions/Completion /usr/share/zsh/functions/Completion/Base /usr/share/zsh/functions/Completion/Linux /usr/share/zsh/functions/Completion/Unix /usr/share/zsh/functions/Completion/X /usr/share/zsh/functions/Completion/Zsh /usr/share/zsh/functions/Exceptions /usr/share/zsh/functions/MIME /usr/share/zsh/functions/Math /usr/share/zsh/functions/Misc /usr/share/zsh/functions/Newuser /usr/share/zsh/functions/Prompts /usr/share/zsh/functions/TCP /usr/share/zsh/functions/VCS_Info /usr/share/zsh/functions/VCS_Info/Backends /usr/share/zsh/functions/Zftp /usr/share/zsh/functions/Zle /home/zach/.zfunc
+3 -2
View File
@@ -1,4 +1,5 @@
import QtQuick import QtQuick
import Quickshell
import ZShell.Components import ZShell.Components
Item { Item {
@@ -19,7 +20,7 @@ Item {
} }
ShaderEffectSource { ShaderEffectSource {
id: source id: src
hideSource: true hideSource: true
sourceItem: contentItem sourceItem: contentItem
@@ -27,7 +28,7 @@ Item {
RoundedMask { RoundedMask {
anchors.fill: parent anchors.fill: parent
source: source source: src
topLeftRadius: root.topLeftRadius topLeftRadius: root.topLeftRadius
topRightRadius: root.topRightRadius topRightRadius: root.topRightRadius
bottomRightRadius: root.bottomRightRadius bottomRightRadius: root.bottomRightRadius
+23
View File
@@ -0,0 +1,23 @@
import QtQuick
import qs.Services
Flickable {
id: root
interactive: !ShellState.forActive().isDrawing
rebound: Transition {
Anim {
properties: "x,y"
type: Anim.FastEffects
}
}
readonly property KineticWheelScroller _scroller: KineticWheelScroller {
id: wheelScroller
anchors.fill: parent
parent: root
z: 12
flickable: root
}
}
+654
View File
@@ -0,0 +1,654 @@
import QtQuick
// Taken from hyprfm: https://github.com/soyeb-jim285/hyprfm/blob/main/src/qml/views/KineticWheelScroller.qml
// Adapted to fit this project where needed.
// Deliberately not a MouseArea: a MouseArea claims the arrow cursor for its
// whole area, which silently killed every cursorShape in the views this
// overlays (column resize grips, pointer hands). A WheelHandler on a plain
// Item takes wheel events without touching the cursor.
Item {
id: root
required property var flickable
property real wheelStep: 56
property real mouseWheelMultiplier: 1.0
property real touchpadMultiplier: 1.35
property real minVelocity: 480
property real maxVelocity: 5200
property real kineticGain: 1.35
property real smoothing: 0.72
property real edgeEpsilon: 1.0
// Browser-style overscroll: larger visible rubber-band, softer
// resistance, and a springy settle on release.
property real overshootLimit: 140
property real overshootResistance: 0.38
property real maxOvershootInput: 520
// Bounce magnitude per unit of impact velocity. Keep this deliberately
// small: slow edge hits should settle, not fire a visibly faster bounce.
property real kineticBounceGain: 0.018
property real maxKineticBounceInput: 90
// Minimum impact velocity that actually produces a visible bounce.
// Very slow landings skip bounce entirely — matches what browsers do
// when a fling decays to near-zero before reaching the edge.
property real minBounceVelocity: 260
property real velocity: 0
property double lastWheelTimestamp: 0
property bool kineticCandidate: false
property real overshootInput: 0
property real overshootReleaseVelocity: 0
property real pendingKineticBounceVelocity: 0
// Set by runKineticGlide when the glide will clamp against an edge.
// It is the glide velocity at the instant of impact, which drives the
// elastic bounce amount and duration — browser-style: slow impact
// makes a small gentle bounce; fast impact makes a big springy one.
property real pendingImpactVelocity: 0
// Browser-style exponential-feeling glide replaces the linear
// flickDeceleration decay. Tail uses OutQuart — soft landing.
property real kineticTau: 0.42
property real mouseGlideTime: 0.09 // decay time constant (s); higher = longer coast
property real mouseAccelWindow: 140 // ms between notches that counts as "continuous"
property real mouseAccelStep: 0.15 // extra impulse multiplier per consecutive notch
property real mouseMaxAccel: 2.5
property real mouseSnap: 0.2 // fraction of each notch applied instantly (0 = fully glided, 1 = no smoothing)
property real mouseVelocity: 0
property int mouseDir: 0
property int mouseStreak: 0
property double mouseLastTime: 0
// Escape hatch if auto-detection misjudges a device
property bool forceMouseWheel: false
readonly property real visualOvershoot: overshootInput < 0 ? rubberDistance(-overshootInput) : -rubberDistance(overshootInput)
signal scrollStarted
WheelHandler {
id: wheelHandler
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
// `blocking` decides whether the event continues to the Flickable
// underneath; handleWheel() flips it per event (the old MouseArea
// code toggled wheel.accepted for the same purpose).
onWheel: wheel => root.handleWheel(wheel)
}
function isMouseWheel(wheel) {
if (forceMouseWheel)
return true;
var ay = wheel.angleDelta.y;
if (ay === 0)
return false;
return Math.abs(ay) % 120 === 0;
}
function minimumContentY() {
if (!flickable)
return 0;
return flickable.originY;
}
function maximumContentY() {
if (!flickable)
return 0;
return Math.max(minimumContentY(), minimumContentY() + flickable.contentHeight - flickable.height);
}
function boundedContentY(value) {
return Math.max(minimumContentY(), Math.min(maximumContentY(), value));
}
function rubberDistance(distance) {
if (distance <= 0)
return 0;
var scaled = (distance * overshootResistance) / overshootLimit;
return overshootLimit * (1 - 1 / (1 + scaled));
}
function setOvershootInput(value) {
overshootInput = Math.max(-maxOvershootInput, Math.min(maxOvershootInput, value));
}
function hasOvershoot() {
return Math.abs(overshootInput) > edgeEpsilon;
}
function absorbOvershoot(delta) {
if (!hasOvershoot())
return delta;
var sameDirection = (overshootInput < 0 && delta < 0) || (overshootInput > 0 && delta > 0);
if (sameDirection) {
setOvershootInput(overshootInput + delta);
return 0;
}
var combined = overshootInput + delta;
if ((overshootInput < 0 && combined < 0) || (overshootInput > 0 && combined > 0)) {
setOvershootInput(combined);
return 0;
}
setOvershootInput(0);
return combined;
}
function deltaFor(wheel) {
var rawDelta;
if (isMouseWheel(wheel))
rawDelta = (wheel.angleDelta.y / 120.0) * wheelStep * mouseWheelMultiplier;
else
rawDelta = wheel.pixelDelta.y !== 0 ? wheel.pixelDelta.y * touchpadMultiplier : (wheel.angleDelta.y / 120.0) * wheelStep * mouseWheelMultiplier;
return wheel.inverted ? rawDelta : -rawDelta;
}
function shouldUseKinetic(wheel, gap) {
return wheel.pixelDelta.y !== 0 || (wheel.phase !== Qt.NoScrollPhase && wheel.angleDelta.y === 0);
}
function scrollBy(delta) {
if (!flickable || Math.abs(delta) < 0.01)
return;
kineticBounceSequence.stop();
bounceBackAnimation.stop();
var remainingDelta = absorbOvershoot(delta);
if (Math.abs(remainingDelta) < 0.01)
return;
var minY = minimumContentY();
var maxY = maximumContentY();
var next = flickable.contentY + remainingDelta;
if (next < minY) {
flickable.contentY = minY;
setOvershootInput(overshootInput + next - minY);
return;
}
if (next > maxY) {
flickable.contentY = maxY;
setOvershootInput(overshootInput + next - maxY);
return;
}
flickable.contentY = next;
}
function remainingDistanceFor(velocityValue) {
if (!flickable)
return 0;
if (velocityValue > 0)
return Math.max(0, maximumContentY() - flickable.contentY);
if (velocityValue < 0)
return Math.max(0, flickable.contentY - minimumContentY());
return 0;
}
function clampedContentVelocity(velocityValue) {
if (!flickable || velocityValue === 0)
return 0;
var remaining = remainingDistanceFor(velocityValue);
if (remaining <= edgeEpsilon)
return 0;
var maxAllowed = Math.sqrt(2 * flickable.flickDeceleration * remaining);
var magnitude = Math.min(Math.abs(velocityValue), maxAllowed);
return velocityValue < 0 ? -magnitude : magnitude;
}
function resetState() {
velocity = 0;
lastWheelTimestamp = 0;
kineticCandidate = false;
pendingKineticBounceVelocity = 0;
pendingImpactVelocity = 0;
overshootReleaseVelocity = 0;
finishTimer.stop();
kineticAnim.stop();
kineticBounceSequence.stop();
bounceBackAnimation.stop();
stopMouseGlide();
}
function bounceBack() {
if (!hasOvershoot()) {
setOvershootInput(0);
overshootReleaseVelocity = 0;
return;
}
var mag = Math.min(1.0, Math.abs(overshootInput) / maxOvershootInput);
var speed = Math.abs(overshootReleaseVelocity);
var duration = speed >= minBounceVelocity ? Math.round(Math.max(280, Math.min(620, Math.abs(overshootInput) / speed * 1600))) : Math.round(380 + mag * 260);
kineticBounceSequence.stop();
bounceBackAnimation.stop();
bounceBackAnimation.duration = duration;
bounceBackAnimation.from = overshootInput;
bounceBackAnimation.to = 0;
overshootReleaseVelocity = 0;
bounceBackAnimation.start();
}
function triggerKineticBounce(contentVelocity) {
if (!flickable || contentVelocity === 0)
return;
var atTop = flickable.contentY <= minimumContentY() + edgeEpsilon;
var atBottom = flickable.contentY >= maximumContentY() - edgeEpsilon;
if ((contentVelocity < 0 && !atTop) || (contentVelocity > 0 && !atBottom))
return;
var speed = Math.abs(contentVelocity);
var effectiveVelocity = contentVelocity;
if (speed < minBounceVelocity)
effectiveVelocity = (contentVelocity < 0 ? -1 : 1) * Math.max(90, speed * 0.6);
var target = Math.max(-maxKineticBounceInput, Math.min(maxKineticBounceInput, effectiveVelocity * kineticBounceGain));
if (Math.abs(target) < 2.5)
target = contentVelocity < 0 ? -2.5 : 2.5;
var mag = Math.min(1.0, Math.abs(target) / maxKineticBounceInput);
var inDur = Math.round(150 + mag * 120);
var outDur = Math.round(320 + mag * 300);
kineticBounceSequence.stop();
bounceBackAnimation.stop();
overshootReleaseVelocity = 0;
overshootInput = 0;
kineticBounceIn.from = 0;
kineticBounceIn.to = target;
kineticBounceIn.duration = inDur;
kineticBounceOut.from = target;
kineticBounceOut.to = 0;
kineticBounceOut.duration = outDur;
kineticBounceSequence.start();
}
function stopAndSettle() {
kineticAnim.stop();
if (flickable && flickable.flicking)
flickable.cancelFlick();
resetState();
bounceBack();
}
function runKineticGlide(contentVelocity) {
// contentVelocity > 0 means the content should scroll downward
// (contentY increases), matching deltaFor()'s sign convention.
if (!flickable || contentVelocity === 0) {
pendingImpactVelocity = 0;
return false;
}
var current = flickable.contentY;
var unclampedTarget = current + contentVelocity * kineticTau;
var target = boundedContentY(unclampedTarget);
var distUnclamped = Math.abs(unclampedTarget - current);
var distActual = Math.abs(target - current);
if (distActual < 4) {
pendingImpactVelocity = 0;
return false;
}
// If the glide is clipped by an edge, compute the velocity at the
// moment of impact using the OutQuart velocity profile. That is
// the velocity the bounce should feed on — gentle impacts get
// gentle bounces, hard impacts get big springy ones.
var hitsEdge = distActual + 0.5 < distUnclamped;
if (hitsEdge && distUnclamped > 0) {
var f = distActual / distUnclamped; // fraction covered
// OutQuart: y = 1 - (1-x)^4, so velocity fraction at distance
// fraction f equals (1-f)^(3/4) (times the constant peak).
var velFraction = Math.pow(Math.max(0, 1 - f), 0.75);
pendingImpactVelocity = contentVelocity * velFraction;
} else {
pendingImpactVelocity = 0;
}
kineticAnim.stop();
kineticAnim.from = current;
kineticAnim.to = target;
// Duration from distance and velocity; clamped so short flicks
// stay snappy and long flicks don't run forever.
kineticAnim.duration = Math.round(Math.min(1400, Math.max(180, distActual / Math.abs(contentVelocity) * 1800)));
kineticAnim.start();
return true;
}
function atEdgeFor(contentVelocity) {
if (!flickable)
return false;
if (contentVelocity > 0)
return flickable.contentY >= maximumContentY() - edgeEpsilon;
if (contentVelocity < 0)
return flickable.contentY <= minimumContentY() + edgeEpsilon;
return false;
}
function finishScroll() {
if (!flickable)
return;
finishTimer.stop();
if (hasOvershoot()) {
velocity = 0;
lastWheelTimestamp = 0;
kineticCandidate = false;
pendingKineticBounceVelocity = 0;
pendingImpactVelocity = 0;
kineticAnim.stop();
bounceBack();
return;
}
var rawContentVelocity = Math.max(-maxVelocity, Math.min(maxVelocity, velocity * kineticGain));
var contentVelocity = clampedContentVelocity(rawContentVelocity);
var useKinetic = kineticCandidate && Math.abs(rawContentVelocity) >= minVelocity;
velocity = 0;
lastWheelTimestamp = 0;
kineticCandidate = false;
if (useKinetic) {
if (atEdgeFor(rawContentVelocity)) {
// Already at the edge — fling bounces immediately.
pendingKineticBounceVelocity = 0;
triggerKineticBounce(rawContentVelocity);
} else if (Math.abs(contentVelocity) >= minVelocity && runKineticGlide(contentVelocity)) {
// Glide started — bounce fires on kineticAnim.onFinished
// if it lands at an edge.
pendingKineticBounceVelocity = contentVelocity;
} else {
pendingKineticBounceVelocity = 0;
flickable.returnToBounds();
}
} else {
pendingKineticBounceVelocity = 0;
flickable.returnToBounds();
}
}
function handleWheel(wheel) {
wheelHandler.blocking = true;
if (!enabled || !visible || !flickable || !flickable.visible || !flickable.interactive) {
wheelHandler.blocking = false;
return;
}
if (wheel.modifiers !== Qt.NoModifier) {
wheelHandler.blocking = false;
return;
}
if (maximumContentY() <= minimumContentY()) {
wheelHandler.blocking = false;
return;
}
var delta = deltaFor(wheel);
if (delta === 0) {
wheelHandler.blocking = false;
return;
}
// Cancel any in-flight glide — user input always wins, position
// stays where it is, then new delta is applied from there.
if (kineticAnim.running) {
pendingKineticBounceVelocity = 0;
pendingImpactVelocity = 0;
kineticAnim.stop();
}
kineticBounceSequence.stop();
bounceBackAnimation.stop();
if (flickable.flicking) {
pendingKineticBounceVelocity = 0;
pendingImpactVelocity = 0;
flickable.cancelFlick();
}
var now = Date.now();
var isTouchpad = !isMouseWheel(wheel) && (wheel.pixelDelta.y !== 0 || (wheel.phase !== Qt.NoScrollPhase && wheel.angleDelta.y === 0));
console.log("wheel", wheel.angleDelta.y, wheel.pixelDelta.y, wheel.phase);
if (!isTouchpad) {
velocity = 0;
lastWheelTimestamp = 0;
kineticCandidate = false;
scrollStarted();
handleMouseDelta(delta, now);
return;
}
// Touchpad path (unchanged below), but cancel any mouse glide first
stopMouseGlide();
var gap = lastWheelTimestamp > 0 ? now - lastWheelTimestamp : 16;
var dt = Math.max(8, Math.min(24, gap));
var instantVelocity = (delta / dt) * 1000;
kineticCandidate = kineticCandidate || shouldUseKinetic(wheel, gap);
scrollStarted();
scrollBy(delta);
if (hasOvershoot()) {
overshootReleaseVelocity = Math.max(-maxVelocity, Math.min(maxVelocity, instantVelocity));
velocity = 0;
kineticCandidate = false;
lastWheelTimestamp = now;
finishTimer.restart();
return;
}
velocity = lastWheelTimestamp <= 0 ? instantVelocity : velocity * (1.0 - smoothing) + instantVelocity * smoothing;
lastWheelTimestamp = now;
finishTimer.restart();
if (wheel.phase === Qt.ScrollEnd)
finishScroll();
}
function stopMouseGlide() {
mouseGlide.stop();
mouseVelocity = 0;
mouseDir = 0;
mouseStreak = 0;
mouseLastTime = 0;
}
function handleMouseDelta(delta, now) {
var dir = delta > 0 ? 1 : -1;
// Edge or active overshoot: existing rubber-band path
if (hasOvershoot() || atEdgeFor(dir)) {
stopMouseGlide();
scrollBy(delta);
overshootReleaseVelocity = 0;
finishTimer.restart();
return;
}
var continuous = dir === mouseDir && (now - mouseLastTime) <= mouseAccelWindow;
if (!continuous)
mouseVelocity = 0; // new gesture or reversal: start clean
mouseStreak = continuous ? mouseStreak + 1 : 0;
// Compute the gap BEFORE overwriting mouseLastTime
var gap = mouseLastTime > 0 ? Math.max(12, Math.min(mouseAccelWindow, now - mouseLastTime)) : 0;
mouseDir = dir;
mouseLastTime = now;
var accel = Math.min(mouseMaxAccel, 1 + mouseStreak * mouseAccelStep);
// --- velocity update (this is the part that changed) ---
// Part of the notch lands immediately
var instant = delta * mouseSnap;
flickable.contentY = boundedContentY(flickable.contentY + instant);
// The rest becomes glide velocity, stacked onto the current velocity
var glide = delta * (1 - mouseSnap) * accel;
var v = mouseVelocity + glide / mouseGlideTime;
// While notches keep arriving, don't fall below the rate they imply
if (continuous && gap > 0) {
var rate = glide / (gap / 1000);
if (Math.abs(rate) > Math.abs(v))
v = rate;
}
mouseVelocity = Math.max(-maxVelocity, Math.min(maxVelocity, v));
// --- end velocity update ---
if (!mouseGlide.running)
mouseGlide.start();
}
function stepMouseGlide(dt) {
if (!flickable || flickable.dragging) {
stopMouseGlide();
return;
}
dt = Math.min(dt, 0.05);
var decay = Math.exp(-dt / mouseGlideTime);
// Exact integral of v0 * e^(-t/tau) over dt, so it's frame-rate independent
var step = mouseVelocity * mouseGlideTime * (1 - decay);
var next = flickable.contentY + step;
var minY = minimumContentY();
var maxY = maximumContentY();
if (next <= minY || next >= maxY) {
var impact = mouseVelocity;
flickable.contentY = next <= minY ? minY : maxY;
mouseGlide.stop();
mouseVelocity = 0;
mouseDir = 0;
mouseStreak = 0;
triggerKineticBounce(impact); // bounce scales with the real impact speed
return;
}
flickable.contentY = next;
mouseVelocity *= decay;
if (Math.abs(mouseVelocity) < 40) // was 8
stopMouseGlide();
}
FrameAnimation {
id: mouseGlide
onTriggered: root.stepMouseGlide(frameTime)
}
Timer {
id: finishTimer
interval: 45
repeat: false
onTriggered: root.finishScroll()
}
NumberAnimation {
id: kineticAnim
target: root.flickable
property: "contentY"
easing.type: Easing.OutQuart
onFinished: {
// Prefer the impact-time velocity (computed in runKineticGlide)
// so the bounce matches the residual energy at the moment the
// glide hit the edge — slow landings → soft bounce, fast
// landings → big springy bounce.
var bv = Math.abs(root.pendingImpactVelocity) > root.edgeEpsilon ? root.pendingImpactVelocity : root.pendingKineticBounceVelocity;
root.pendingImpactVelocity = 0;
root.pendingKineticBounceVelocity = 0;
if (Math.abs(bv) > root.edgeEpsilon)
root.triggerKineticBounce(bv);
}
}
NumberAnimation {
id: bounceBackAnimation
target: root
property: "overshootInput"
easing.type: Easing.OutCubic
}
SequentialAnimation {
id: kineticBounceSequence
NumberAnimation {
id: kineticBounceIn
target: root
property: "overshootInput"
easing.type: Easing.OutCubic
}
NumberAnimation {
id: kineticBounceOut
target: root
property: "overshootInput"
easing.type: Easing.OutCubic
}
}
Translate {
id: overshootTranslate
y: root.visualOvershoot
}
Connections {
target: root.flickable
function onInteractiveChanged() {
if (!root.flickable.interactive)
root.stopAndSettle();
}
function onContentHeightChanged() {
if (!root.flickable.moving) {
root.flickable.contentY = root.boundedContentY(root.flickable.contentY);
root.bounceBack();
}
}
function onHeightChanged() {
if (!root.flickable.moving) {
root.flickable.contentY = root.boundedContentY(root.flickable.contentY);
root.bounceBack();
}
}
function onVisibleChanged() {
if (!root.flickable.visible)
root.stopAndSettle();
}
function onMovementEnded() {
if (Math.abs(root.pendingKineticBounceVelocity) > root.edgeEpsilon) {
var bounceVelocity = root.pendingKineticBounceVelocity;
root.pendingKineticBounceVelocity = 0;
root.triggerKineticBounce(bounceVelocity);
}
}
}
Component.onCompleted: {
if (!flickable)
return;
flickable.flickDeceleration = 2800;
flickable.maximumFlickVelocity = Math.max(flickable.maximumFlickVelocity, maxVelocity);
flickable.contentItem.transform = [overshootTranslate];
}
}
+3 -5
View File
@@ -35,7 +35,7 @@ MouseArea {
cursorShape: expanded ? Qt.ArrowCursor : undefined cursorShape: expanded ? Qt.ArrowCursor : undefined
enabled: expanded enabled: expanded
hoverEnabled: expanded hoverEnabled: expanded
layer.enabled: opacity < 1 layer.enabled: false
opacity: expanded ? 1 : 0 opacity: expanded ? 1 : 0
parent: { parent: {
const win = QsWindow.window; const win = QsWindow.window;
@@ -102,8 +102,7 @@ MouseArea {
yScale: root.expanded ? 1 : 0.1 yScale: root.expanded ? 1 : 0.1
Behavior on yScale { Behavior on yScale {
Anim { Anim {}
}
} }
} }
@@ -140,8 +139,7 @@ MouseArea {
radius: menu.radius - column.anchors.margins radius: menu.radius - column.anchors.margins
Behavior on radius { Behavior on radius {
Anim { Anim {}
}
} }
StateLayer { StateLayer {
+15 -261
View File
@@ -4,14 +4,11 @@ pragma ComponentBehavior: Bound
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Services.Notifications import Quickshell.Services.Notifications
import Quickshell.Hyprland import ZShell.Services
import QtQuick
import ZShell
import qs.Components import qs.Components
import qs.Modules
import qs.Helpers import qs.Helpers
import qs.Services
import qs.Paths import qs.Paths
import qs.Services
import ZShell.Config import ZShell.Config
Singleton { Singleton {
@@ -19,11 +16,9 @@ Singleton {
readonly property var appCooldownMap: new Map() readonly property var appCooldownMap: new Map()
property alias dnd: props.dnd property alias dnd: props.dnd
property list<Notif> list: [] property alias list: store.rows
property bool loaded readonly property list<NotificationRow> notClosed: store.rows.filter(n => !n.closed)
readonly property list<Notif> notClosed: list.filter(n => !n.closed) readonly property list<NotificationRow> popups: store.rows.filter(n => n.popup)
readonly property list<Notif> popups: list.filter(n => n.popup)
property alias server: server
function shouldThrottle(appName: string): bool { function shouldThrottle(appName: string): bool {
if (props.dnd || ShellState.anySidebarOpen()) if (props.dnd || ShellState.anySidebarOpen())
@@ -46,33 +41,6 @@ Singleton {
return true; return true;
} }
onListChanged: {
if (loaded) {
saveTimer.restart();
}
}
Timer {
id: saveTimer
interval: 1000
onTriggered: storage.setText(JSON.stringify(root.notClosed.map(n => ({
time: n.time,
id: n.id,
summary: n.summary,
body: n.body,
appIcon: n.appIcon,
appName: n.appName,
image: n.image,
expireTimeout: n.expireTimeout,
urgency: n.urgency,
resident: n.resident,
hasActionIcons: n.hasActionIcons,
actions: n.actions
}))))
}
PersistentProperties { PersistentProperties {
id: props id: props
@@ -81,6 +49,12 @@ Singleton {
reloadableId: "notifs" reloadableId: "notifs"
} }
NotificationStore {
id: store
path: `${Paths.state}/notifs.sqlite`
}
NotificationServer { NotificationServer {
id: server id: server
@@ -89,40 +63,12 @@ Singleton {
bodyImagesSupported: true bodyImagesSupported: true
bodyMarkupSupported: true bodyMarkupSupported: true
imageSupported: true imageSupported: true
keepOnReload: false keepOnReload: true
persistenceSupported: true persistenceSupported: true
onNotification: notif => { onNotification: notif => {
notif.tracked = true; notif.tracked = true;
store.insert(notif, root.shouldThrottle(notif.appName));
const is_popup = root.shouldThrottle(notif.appName);
const comp = notifComp.createObject(root, {
popup: is_popup,
notification: notif
});
root.list = [comp, ...root.list];
}
}
FileView {
id: storage
path: `${Paths.state}/notifs.json`
printErrors: false
onLoadFailed: err => {
if (err === FileViewError.FileNotFound) {
root.loaded = true;
Qt.callLater(() => setText("[]"));
}
}
onLoaded: {
const data = JSON.parse(text());
for (const notif of data)
root.list.push(notifComp.createObject(root, notif));
root.list.sort((a, b) => b.time - a.time);
root.loaded = true;
} }
} }
@@ -130,16 +76,12 @@ Singleton {
description: "Clear all notifications" description: "Clear all notifications"
name: "clearnotifs" name: "clearnotifs"
onPressed: { onPressed: store.clear()
for (const notif of root.list.slice())
notif.close();
}
} }
IpcHandler { IpcHandler {
function clear(): void { function clear(): void {
for (const notif of root.list.slice()) store.clear();
notif.close();
} }
function disableDnd(): void { function disableDnd(): void {
@@ -160,192 +102,4 @@ Singleton {
target: "notifs" target: "notifs"
} }
Component {
id: notifComp
Notif {
}
}
component Notif: QtObject {
id: notif
property list<var> actions
property string appIcon
property string appName
property string body
property string cachedImageSource: ""
property bool cachingImage: false
property bool closed
readonly property Connections conn: Connections {
function onActionsChanged(): void {
notif.actions = notif.notification.actions.map(a => ({
identifier: a.identifier,
text: a.text,
invoke: () => a.invoke()
}));
}
function onAppIconChanged(): void {
notif.appIcon = notif.notification.appIcon;
}
function onAppNameChanged(): void {
notif.appName = notif.notification.appName;
}
function onBodyChanged(): void {
notif.body = notif.notification.body;
}
function onClosed(): void {
notif.close();
}
function onExpireTimeoutChanged(): void {
notif.expireTimeout = notif.notification.expireTimeout;
}
function onHasActionIconsChanged(): void {
notif.hasActionIcons = notif.notification.hasActionIcons;
}
function onHintsChanged(): void {
notif.hints = notif.notification.hints;
}
function onImageChanged(): void {
notif.imageSource = notif.notification.image || "";
notif.image = notif.imageSource;
notif.cacheImageIfNeeded();
}
function onResidentChanged(): void {
notif.resident = notif.notification.resident;
}
function onSummaryChanged(): void {
notif.summary = notif.notification.summary;
}
function onUrgencyChanged(): void {
notif.urgency = notif.notification.urgency;
}
target: notif.notification
}
property real expireTimeout: 5
property bool hasActionIcons
property var hints
property string image
property string imageSource
property var locks: new Set()
property string notifId
property Notification notification
property bool popup
property bool resident
property string summary
property date time: new Date()
readonly property string timeStr: {
const diff = Time.date.getTime() - time.getTime();
const m = Math.floor(diff / 60000);
if (m < 1)
return qsTr("now");
const h = Math.floor(m / 60);
const d = Math.floor(h / 24);
if (d > 0)
return `${d}d`;
if (h > 0)
return `${h}h`;
return `${m}m`;
}
readonly property Timer timer: Timer {
property bool paused: false
property int remainingTime: totalTime
property int totalTime: Config.notifs.defaultExpireTimeout
interval: 50
repeat: true
running: !paused
onTriggered: {
remainingTime -= interval;
if (remainingTime <= 0) {
remainingTime = 0;
notif.popup = false;
stop();
}
}
}
property int urgency: NotificationUrgency.Normal
function cacheImageIfNeeded(): void {
const source = imageSource;
if (!source || cachingImage)
return;
if (cachedImageSource === source)
return;
cachingImage = true;
ZShellIo.cacheImage(Qt.resolvedUrl(source), Paths.notifimagecache, (path, url) => {
cachedImageSource = source;
image = path;
cachingImage = false;
}, () => {
cachingImage = false;
});
}
function close(): void {
closed = true;
if (locks.size === 0 && root.list.includes(this)) {
root.list = root.list.filter(n => n !== this);
notification?.dismiss();
destroy();
}
}
function lock(item: Item): void {
locks.add(item);
}
function unlock(item: Item): void {
locks.delete(item);
if (closed)
close();
}
Component.onCompleted: {
if (!notification)
return;
notifId = notification.id;
summary = notification.summary;
body = notification.body;
appIcon = notification.appIcon;
appName = notification.appName;
imageSource = notification.image || "";
image = imageSource;
hints = notification.hints;
expireTimeout = notification.expireTimeout;
urgency = notification.urgency;
resident = notification.resident;
hasActionIcons = notification.hasActionIcons;
actions = notification.actions.map(a => ({
identifier: a.identifier,
text: a.text,
invoke: () => a.invoke()
}));
cacheImageIfNeeded();
}
}
} }
+7
View File
@@ -4,12 +4,16 @@ import Quickshell
import QtQuick import QtQuick
import ZShell.Internal import ZShell.Internal
import qs.Components import qs.Components
import qs.Services
Item { Item {
id: root id: root
readonly property alias content: contentLoader.item readonly property alias content: contentLoader.item
required property ScreenState screenState required property ScreenState screenState
property rect holeRect
property real holeRadius: 0
Loader { Loader {
id: contentLoader id: contentLoader
@@ -18,6 +22,9 @@ Item {
anchors.fill: parent anchors.fill: parent
sourceComponent: StrokeCanvas { sourceComponent: StrokeCanvas {
anchors.fill: parent
hole: root.holeRect
holeRadius: root.holeRadius
penColor: root.screenState.penColor penColor: root.screenState.penColor
penWidth: root.screenState.penWidth penWidth: root.screenState.penWidth
} }
+95
View File
@@ -0,0 +1,95 @@
import Quickshell
import QtQuick
import ZShell.Config
import qs.Components
CustomMouseArea {
id: root
required property real borderThickness
required property Drawing drawing
required property Panels panels
required property ScreenState screenState
required property EdgeGeometry geometry
function inLeftPanel(panel: Item, x: real, y: real): bool {
return x < panel.x + panel.width + geometry.insetLeft(borderThickness) && withinPanelHeight(panel, x, y);
}
function withinPanelHeight(panel: Item, x: real, y: real): bool {
const panelY = panel.y + geometry.insetTop(borderThickness);
return y >= panelY && y <= panelY + panel.height;
}
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: Qt.BlankCursor
enabled: z > 0
hoverEnabled: true
focus: root.screenState.isDrawing
z: 2
Binding {
target: root.drawing.content
property: "hoverPoint"
value: Qt.point(root.mouseX, root.mouseY)
}
Keys.onPressed: e => {
const mod = e.modifiers;
const key = e.key;
if ((key == Qt.Key_Z) && (mod & Qt.ControlModifier) && (mod & Qt.ShiftModifier)) {
root.drawing.content.redo();
e.accepted = true;
} else if ((key == Qt.Key_Z) && (mod & Qt.ControlModifier)) {
root.drawing.content.undoLastGroup();
e.accepted = true;
}
}
onPositionChanged: event => {
const x = event.x;
const y = event.y;
if (!drawing.content.hoverVisible)
drawing.content.showHover();
if (event.buttons & Qt.LeftButton) {
drawing.content.appendPoint(x, y);
return;
}
if (!(event.buttons & Qt.LeftButton) && inLeftPanel(panels.drawing, x, y)) {
z = -2;
panels.drawing.expand();
drawing.content.hideHover();
}
}
onPressed: event => {
const x = event.x;
const y = event.y;
panels.drawing.collapse();
if (event.buttons & Qt.LeftButton) {
panels.drawing.collapse();
drawing.content.beginStroke(x, y);
return;
}
if (event.button & Qt.RightButton) {
drawing.content.clear();
return;
}
}
onReleased: event => {
const x = event.x;
const y = event.y;
if (event.button & Qt.LeftButton) {
drawing.content.endStroke();
drawing.content.showHover();
}
}
}
+121 -198
View File
@@ -1,25 +1,33 @@
import Quickshell import Quickshell
import QtQuick import QtQuick
import ZShell.Config import ZShell.Config
import ZShell.Internal
import qs.Components import qs.Components
import qs.Helpers import qs.Helpers
import qs.Modules.Bar.Popouts import qs.Modules.Bar.Popouts
Item { CustomMouseArea {
id: root id: root
required property Drawing drawing
required property DrawingInput input
required property Item bar required property Item bar
required property real borderThickness required property real borderThickness
property bool dashboardShortcutActive
required property Drawing drawing
required property EdgeGeometry geometry required property EdgeGeometry geometry
property bool osdShortcutActive
required property Panels panels required property Panels panels
required property Wrapper popouts required property Wrapper popouts
required property ShellScreen screen required property ShellScreen screen
required property ScreenState screenState
property bool dashboardShortcutActive
property point dragStart
property bool osdShortcutActive
property bool setInitialPoint: false
property bool singleGestureTriggered: false property bool singleGestureTriggered: false
property bool utilitiesShortcutActive property bool utilitiesShortcutActive
required property ScreenState screenState
function inBottomPanel(panel: Item, x: real, y: real): bool { function inBottomPanel(panel: Item, x: real, y: real): bool {
const panelHeight = panel.height * (1 - (panel.offsetScale ?? 0)); const panelHeight = panel.height * (1 - (panel.offsetScale ?? 0));
@@ -56,124 +64,116 @@ Item {
return x >= panelX && x <= panelX + panel.width; return x >= panelX && x <= panelX + panel.width;
} }
acceptedButtons: bar.fullscreen ? Qt.NoButton : Qt.AllButtons
anchors.fill: parent anchors.fill: parent
hoverEnabled: true
DragHandler { onContainsMouseChanged: {
id: singleHandler if (!containsMouse) {
if (!osdShortcutActive) {
cursorShape: (active && centroid.pressPosition.y < root.bar.implicitHeight) ? Qt.ClosedHandCursor : undefined screenState.osd = false;
dragThreshold: 0 panels.osd.hovered = false;
enabled: !root.screenState.isDrawing
grabPermissions: PointerHandler.CanTakeOverFromHandlersOfSameType | PointerHandler.ApprovesTakeOverByAnything
maximumPointCount: 1
minimumPointCount: 1
target: null
onActiveChanged: {
if (!active)
root.singleGestureTriggered = false;
}
onCentroidChanged: {
const x = centroid.position.x;
const y = centroid.position.y;
const pressX = centroid.pressPosition.x;
const pressY = centroid.pressPosition.y;
const dragX = x - pressX;
const dragY = y - pressY;
if (pressY >= root.screen.height - Config.bar.border && pressX > root.screen.width / 5 && dragY < -Config.launcher.dragThreshold)
root.screenState.launcher = true;
if (root.singleGestureTriggered)
return;
const inDashboard = root.inTopPanel(root.panels.dashboardWrapper, pressX, pressY);
if (inDashboard && root.withinPanelWidth(root.panels.dashboardWrapper, x, y)) {
if (dragY > Config.dashboard.dragThreshold) {
root.screenState.dashboard = true;
root.singleGestureTriggered = true;
} else if (dragY < -Config.dashboard.dragThreshold) {
root.screenState.dashboard = false;
root.singleGestureTriggered = true;
}
} }
if (root.geometry.insetTop(root.borderThickness) > pressY && (Config.dashboard.showOnHover || !inDashboard)) { if (!popouts.currentName.startsWith("traymenu") || Config.bar.tray.showOnHover) {
if (dragY > 20) { popouts.hasCurrent = false;
root.screenState.settings = true;
root.singleGestureTriggered = true;
} else if (dragY < -20) {
root.screenState.settings = false;
root.singleGestureTriggered = true;
}
} }
if (pressY > root.screen.height - root.geometry.insetBottom(root.borderThickness) && pressX < root.screen.width / 5 && dragY < -50) if (Config.bar.autoHide || bar.fullscreen)
root.screenState.clipboard = true; bar.isHovered = false;
if (!Config.dock.hoverToReveal && pressY > root.screen.height - root.geometry.insetTop(root.borderThickness) && pressX > root.screen.width / 5 && !root.screenState.launcher) if (screenState.dashboard && Config.dashboard.showOnHover)
if (dragY < -10) { screenState.dashboard = false;
root.screenState.dock = true;
root.singleGestureTriggered = true;
}
if (pressX > root.screen.width - Config.bar.border && pressY < (root.screen.height / 2) && dragX < -Config.sidebar.dragThreshold) {
root.screenState.sidebar = true;
root.singleGestureTriggered = true;
}
if (pressX >= root.screen.width - Config.bar.border && pressY > (root.screen.height / 2) && dragX < -20) {
Hypr.dispatch(`hl.dsp.focus({ workspace = 'r+1', on_current_monitor = true })`);
root.singleGestureTriggered = true;
}
if (pressX <= Config.bar.border && dragX > 20) {
Hypr.dispatch(`hl.dsp.focus({ workspace = 'r-1', on_current_monitor = true })`);
root.singleGestureTriggered = true;
}
} }
} }
onPositionChanged: event => {
const x = event.x;
const y = event.y;
const dragX = x - dragStart.x;
const dragY = y - dragStart.y;
PointHandler { if (screenState.isDrawing && !inLeftPanel(panels.drawing, x, y)) {
id: drawingHandler input.z = 2;
panels.drawing.collapse();
}
property bool setInitialPoint: false if (root.bar.fullscreen && !root.bar.isHovered)
unhideTimer.start();
acceptedButtons: Qt.LeftButton | Qt.RightButton if (pressed && dragStart.y >= screen.height - Config.bar.border && dragStart.x > screen.width / 5 && dragY < -Config.launcher.dragThreshold)
enabled: root.screenState.isDrawing && (!root.inLeftPanel(root.panels.drawing, hoverHandler.point.position.x, hoverHandler.point.position.y) || !root.panels.drawing.expanded) screenState.launcher = true;
onActiveChanged: { const entryAt = bar.entryAt(geometry.axisPos(x, y));
if (!active) { const showDashboard = Config.dashboard.showOnHover && inTopPanel(panels.dashboardWrapper, x, y) && (!entryAt || entryAt === "activeWindow");
setInitialPoint = false;
root.drawing.content.endStroke(); if (!dashboardShortcutActive) {
} else { screenState.dashboard = showDashboard;
root.panels.drawing.collapse(); } else if (showDashboard) {
dashboardShortcutActive = false;
}
if (pressed && geometry.insetTop(borderThickness) > dragStart.y && withinPanelWidth(panels.dashboardWrapper, x, y)) {
if (dragY > Config.dashboard.dragThreshold)
screenState.dashboard = true;
else if (dragY < -Config.dashboard.dragThreshold)
screenState.dashboard = false;
}
if (pressed && geometry.insetTop(borderThickness) > dragStart.y && !inTopPanel(panels.dashboardWrapper, dragStart.x, dragStart.y)) {
if (dragY > 20) {
screenState.settings = true;
} else if (dragY < -20) {
screenState.settings = false;
} }
} }
onPointChanged: {
if (!active)
return;
const x = point.position.x;
const y = point.position.y;
const origX = point.pressPosition.x;
const origY = point.pressPosition.y;
if (point.pressedButtons & Qt.RightButton) { if (pressed && dragStart.y > screen.height - geometry.insetBottom(borderThickness) && dragStart.x < screen.width / 5 && dragY < -50)
root.drawing.content.clear(); screenState.clipboard = true;
return;
if (pressed && !Config.dock.hoverToReveal && dragStart.y > screen.height - geometry.insetTop(borderThickness) && dragStart.x > screen.width / 5 && !screenState.launcher)
if (dragY < -10)
screenState.dock = true;
if (pressed && dragStart.x > screen.width - Config.bar.border && dragStart.y < (screen.height / 2) && dragX < -Config.sidebar.dragThreshold)
screenState.sidebar = true;
if (pressed && dragStart.x >= screen.width - Config.bar.border && dragStart.y > (screen.height / 2) && dragX < -20)
Hypr.dispatch(`hl.dsp.focus({ workspace = 'r+1', on_current_monitor = true })`);
if (pressed && dragStart.x <= Config.bar.border && dragX > 20)
Hypr.dispatch(`hl.dsp.focus({ workspace = 'r-1', on_current_monitor = true })`);
if (!screenState.bar && Config.bar.autoHide && geometry.barContains(x, y, true))
bar.isHovered = true;
if (panels.sidebar.offsetScale === 1) {
const showOsd = inRightPanel(panels.osdWrapper, x, y);
if (showOsd) {
osdShortcutActive = false;
panels.osd.hovered = true;
} }
if (x === 0 && y === 0 && origX === 0 && origY === 0) } else {
return; const outOfSidebar = x < width - panels.sidebar.width * (1 - panels.sidebar.offsetScale);
const showOsd = outOfSidebar && inRightPanel(panels.osdWrapper, x, y);
if (!setInitialPoint) { if (!osdShortcutActive) {
setInitialPoint = true; screenState.osd = showOsd;
root.drawing.content.beginStroke(origX, origY); panels.osd.hovered = showOsd;
return; } else if (showOsd) {
osdShortcutActive = false;
panels.osd.hovered = true;
} }
root.drawing.content.appendPoint(x, y);
} }
if (Config.dock.enable && Config.dock.hoverToReveal && !screenState.dock && !screenState.launcher && inBottomPanel(panels.dock, x, y))
screenState.dock = true;
if (geometry.barContains(x, y))
bar.checkPopout(geometry.axisPos(x, y));
}
onPressed: event => {
dragStart = Qt.point(event.x, event.y);
} }
Timer { Timer {
@@ -184,98 +184,20 @@ Item {
running: false running: false
onTriggered: { onTriggered: {
if (hoverHandler.hovered) if (root.containsMouse)
root.bar.isHovered = true; root.bar.isHovered = true;
} }
} }
HoverHandler {
id: hoverHandler
cursorShape: root.screenState.isDrawing && !root.inLeftPanel(root.panels.drawing, point.position.x, point.position.y) ? Qt.BlankCursor : undefined
onHoveredChanged: {
if (!hovered) {
if (!root.osdShortcutActive) {
root.screenState.osd = false;
root.panels.osd.hovered = false;
}
if (!root.popouts.currentName.startsWith("traymenu") || Config.bar.tray.showOnHover) {
root.popouts.hasCurrent = false;
}
if (Config.bar.autoHide || root.bar.fullscreen)
root.bar.isHovered = false;
if (root.screenState.dashboard && Config.dashboard.showOnHover)
root.screenState.dashboard = false;
}
}
onPointChanged: {
const x = point.position.x;
const y = point.position.y;
if (root.screenState.isDrawing) {
if (root.inLeftPanel(root.panels.drawing, x, y) && !(drawingHandler.point.pressedButtons & Qt.LeftButton)) {
root.panels.drawing.expand();
root.drawing.content.hideHover();
return;
}
root.drawing.content.showHover(x, y);
return;
}
if (!root.screenState.bar && Config.bar.autoHide && root.geometry.barContains(x, y, true))
root.bar.isHovered = true;
if (root.bar.fullscreen && !root.bar.isHovered)
unhideTimer.start();
if (root.panels.sidebar.offsetScale === 1) {
const showOsd = root.inRightPanel(root.panels.osdWrapper, x, y);
if (showOsd) {
root.osdShortcutActive = false;
root.panels.osd.hovered = true;
}
} else {
const outOfSidebar = x < root.width - root.panels.sidebar.width * (1 - root.panels.sidebar.offsetScale);
const showOsd = outOfSidebar && root.inRightPanel(root.panels.osdWrapper, x, y);
if (!root.osdShortcutActive) {
root.screenState.osd = showOsd;
root.panels.osd.hovered = showOsd;
} else if (showOsd) {
root.osdShortcutActive = false;
root.panels.osd.hovered = true;
}
}
if (Config.dock.enable && Config.dock.hoverToReveal && !root.screenState.dock && !root.screenState.launcher && root.inBottomPanel(root.panels.dock, x, y))
root.screenState.dock = true;
const inDashboard = root.inTopPanel(root.panels.dashboardWrapper, x, y);
if (Config.dashboard.showOnHover)
root.screenState.dashboard = inDashboard;
if (root.geometry.barContains(x, y))
root.bar.checkPopout(root.geometry.axisPos(x, y));
}
}
Connections { Connections {
function onDashboardChanged() { function onDashboardChanged(): void {
if (root.screenState.dashboard) { if (root.screenState.dashboard) {
const inDashboardArea = root.inTopPanel(root.panels.dashboard, root.mouseX, root.mouseY); const inDashboardArea = root.inTopPanel(root.panels.dashboardWrapper, root.mouseX, root.mouseY);
if (!inDashboardArea) { if (!inDashboardArea) {
root.dashboardShortcutActive = true; root.dashboardShortcutActive = true;
} }
if (root.panels.launcher.x + root.panels.launcher.width > root.panels.dashboardWrapper.x)
root.screenState.launcher = false;
root.screenState.launcher = false;
root.screenState.settings = false; root.screenState.settings = false;
root.screenState.sidebar = false; root.screenState.sidebar = false;
root.screenState.resources = false; root.screenState.resources = false;
@@ -285,12 +207,15 @@ Item {
} }
} }
function onIsDrawingChanged() { function onIsDrawingChanged(): void {
if (!root.screenState.isDrawing) if (root.screenState.isDrawing) {
root.drawing.clear(); root.drawing.content.showHover();
} else {
root.drawing.content.clear();
}
} }
function onLauncherChanged() { function onLauncherChanged(): void {
if (!root.screenState.launcher) { if (!root.screenState.launcher) {
root.dashboardShortcutActive = false; root.dashboardShortcutActive = false;
root.osdShortcutActive = false; root.osdShortcutActive = false;
@@ -305,16 +230,13 @@ Item {
} }
if (root.screenState.launcher) { if (root.screenState.launcher) {
if (root.panels.dashboardWrapper.x < root.panels.launcher.x + root.panels.launcher.width) { root.screenState.dashboard = false;
root.screenState.dashboard = false;
}
root.screenState.dock = false; root.screenState.dock = false;
root.screenState.settings = false; root.screenState.settings = false;
} }
} }
function onOsdChanged() { function onOsdChanged(): void {
if (root.screenState.osd) { if (root.screenState.osd) {
const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY); const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY);
if (!inOsdArea) { if (!inOsdArea) {
@@ -325,7 +247,7 @@ Item {
} }
} }
function onResourcesChanged() { function onResourcesChanged(): void {
if (root.screenState.resources) { if (root.screenState.resources) {
if (root.popouts.currentName.startsWith("audio") || root.popouts.currentName.startsWith("updates")) if (root.popouts.currentName.startsWith("audio") || root.popouts.currentName.startsWith("updates"))
root.popouts.hasCurrent = false; root.popouts.hasCurrent = false;
@@ -335,7 +257,7 @@ Item {
} }
} }
function onSettingsChanged() { function onSettingsChanged(): void {
if (root.screenState.settings) { if (root.screenState.settings) {
root.screenState.resources = false; root.screenState.resources = false;
root.screenState.dashboard = false; root.screenState.dashboard = false;
@@ -345,9 +267,10 @@ Item {
} }
} }
function onSidebarChanged() { function onSidebarChanged(): void {
if (root.screenState.sidebar) { if (root.screenState.sidebar) {
root.screenState.dashboard = false; if (root.panels.dashboardWrapper.x + root.panels.dashboardWrapper.width > root.panels.sidebar.x)
root.screenState.dashboard = false;
root.screenState.settings = false; root.screenState.settings = false;
root.popouts.hasCurrent = false; root.popouts.hasCurrent = false;
} }
-315
View File
@@ -1,315 +0,0 @@
import Quickshell
import QtQuick
import ZShell.Config
import ZShell.Internal
import qs.Components
import qs.Helpers
import qs.Modules.Bar.Popouts
CustomMouseArea {
id: root
required property Item bar
required property real borderThickness
property bool dashboardShortcutActive
property point dragStart
required property Drawing drawing
required property EdgeGeometry geometry
property bool osdShortcutActive
required property Panels panels
required property Wrapper popouts
required property ShellScreen screen
required property ScreenState screenState
property bool setInitialPoint: false
property bool singleGestureTriggered: false
property bool utilitiesShortcutActive
function inBottomPanel(panel: Item, x: real, y: real): bool {
const panelHeight = panel.height * (1 - (panel.offsetScale ?? 0));
return y > root.height - (panelHeight + geometry.insetBottom(borderThickness)) && withinPanelWidth(panel, x, y);
}
function inLeftPanel(panel: Item, x: real, y: real): bool {
return x < panel.x + panel.width + geometry.insetLeft(borderThickness) && withinPanelHeight(panel, x, y);
}
function inPopoutArea(x: real, y: real): bool {
const panel = panels.popoutsWrapper;
if (!geometry.horizontal)
return inLeftPanel(panel, x, y);
return withinPanelWidth(panel, x, y) && withinPanelHeight(panel, x, y);
}
function inRightPanel(panel: Item, x: real, y: real): bool {
return x > panel.x + geometry.insetLeft(borderThickness) && withinPanelHeight(panel, x, y);
}
function inTopPanel(panel: Item, x: real, y: real): bool {
const panelHeight = panel.height * (1 - (panel.offsetScale ?? 0));
return y < geometry.insetTop(borderThickness) + panelHeight && withinPanelWidth(panel, x, y);
}
function withinPanelHeight(panel: Item, x: real, y: real): bool {
const panelY = panel.y + geometry.insetTop(borderThickness);
return y >= panelY && y <= panelY + panel.height;
}
function withinPanelWidth(panel: Item, x: real, y: real): bool {
const panelX = panel.x + geometry.insetLeft(borderThickness);
return x >= panelX && x <= panelX + panel.width;
}
acceptedButtons: bar.fullscreen ? Qt.NoButton : Qt.AllButtons
anchors.fill: parent
hoverEnabled: true
onContainsMouseChanged: {
if (!containsMouse) {
if (!osdShortcutActive) {
screenState.osd = false;
panels.osd.hovered = false;
}
if (!popouts.currentName.startsWith("traymenu") || Config.bar.tray.showOnHover) {
popouts.hasCurrent = false;
}
if (Config.bar.autoHide || bar.fullscreen)
bar.isHovered = false;
if (screenState.dashboard && Config.dashboard.showOnHover)
screenState.dashboard = false;
}
}
onPositionChanged: event => {
const x = event.x;
const y = event.y;
const dragX = x - dragStart.x;
const dragY = y - dragStart.y;
if (screenState.isDrawing) {
if (pressed) {
drawing.content.appendPoint(x, y);
} else {
if (inLeftPanel(panels.drawing, x, y)) {
if (!(pressedButtons & Qt.LeftButton)) {
panels.drawing.expand();
drawing.content.hideHover();
Cursor.clearOverride();
}
} else {
Cursor.setOverride(Qt.BlankCursor);
drawing.content.showHover(x, y);
}
}
if (drawing.content.hoverVisible)
drawing.content.showHover(x, y);
return;
}
if (root.bar.fullscreen && !root.bar.isHovered)
unhideTimer.start();
if (pressed && dragStart.y >= screen.height - Config.bar.border && dragStart.x > screen.width / 5 && dragY < -Config.launcher.dragThreshold)
screenState.launcher = true;
const entryAt = bar.entryAt(geometry.axisPos(x, y));
const showDashboard = Config.dashboard.showOnHover && inTopPanel(panels.dashboardWrapper, x, y) && (!entryAt || entryAt === "activeWindow");
if (!dashboardShortcutActive) {
screenState.dashboard = showDashboard;
} else if (showDashboard) {
dashboardShortcutActive = false;
}
if (pressed && geometry.insetTop(borderThickness) > dragStart.y && withinPanelWidth(panels.dashboardWrapper, x, y)) {
if (dragY > Config.dashboard.dragThreshold)
screenState.dashboard = true;
else if (dragY < -Config.dashboard.dragThreshold)
screenState.dashboard = false;
}
if (pressed && geometry.insetTop(borderThickness) > dragStart.y && !inTopPanel(panels.dashboardWrapper, dragStart.x, dragStart.y)) {
if (dragY > 20) {
screenState.settings = true;
} else if (dragY < -20) {
screenState.settings = false;
}
}
if (pressed && dragStart.y > screen.height - geometry.insetBottom(borderThickness) && dragStart.x < screen.width / 5 && dragY < -50)
screenState.clipboard = true;
if (pressed && !Config.dock.hoverToReveal && dragStart.y > screen.height - geometry.insetTop(borderThickness) && dragStart.x > screen.width / 5 && !screenState.launcher)
if (dragY < -10)
screenState.dock = true;
if (pressed && dragStart.x > screen.width - Config.bar.border && dragStart.y < (screen.height / 2) && dragX < -Config.sidebar.dragThreshold)
screenState.sidebar = true;
if (pressed && dragStart.x >= screen.width - Config.bar.border && dragStart.y > (screen.height / 2) && dragX < -20)
Hypr.dispatch(`hl.dsp.focus({ workspace = 'r+1', on_current_monitor = true })`);
if (pressed && dragStart.x <= Config.bar.border && dragX > 20)
Hypr.dispatch(`hl.dsp.focus({ workspace = 'r-1', on_current_monitor = true })`);
if (!screenState.bar && Config.bar.autoHide && geometry.barContains(x, y, true))
bar.isHovered = true;
if (panels.sidebar.offsetScale === 1) {
const showOsd = inRightPanel(panels.osdWrapper, x, y);
if (showOsd) {
osdShortcutActive = false;
panels.osd.hovered = true;
}
} else {
const outOfSidebar = x < width - panels.sidebar.width * (1 - panels.sidebar.offsetScale);
const showOsd = outOfSidebar && inRightPanel(panels.osdWrapper, x, y);
if (!osdShortcutActive) {
screenState.osd = showOsd;
panels.osd.hovered = showOsd;
} else if (showOsd) {
osdShortcutActive = false;
panels.osd.hovered = true;
}
}
if (Config.dock.enable && Config.dock.hoverToReveal && !screenState.dock && !screenState.launcher && inBottomPanel(panels.dock, x, y))
screenState.dock = true;
if (geometry.barContains(x, y))
bar.checkPopout(geometry.axisPos(x, y));
}
onPressed: event => {
dragStart = Qt.point(event.x, event.y);
if (screenState.isDrawing) {
if (event.button & Qt.RightButton) {
drawing.content.clear();
return;
}
if (!inLeftPanel(panels.drawing, mouseX, mouseY))
drawing.content.beginStroke(event.x, event.y);
}
}
onPressedChanged: {
if (!pressed) {
if (screenState.isDrawing) {
setInitialPoint = false;
drawing.content.endStroke();
if (!inLeftPanel(panels.drawing, mouseX, mouseY))
drawing.content.showHover(mouseX, mouseY);
}
} else {
if (screenState.isDrawing) {
panels.drawing.collapse();
}
}
}
Timer {
id: unhideTimer
interval: Config.bar.revealDelay
repeat: false
running: false
onTriggered: {
if (root.containsMouse)
root.bar.isHovered = true;
}
}
Connections {
function onDashboardChanged(): void {
if (root.screenState.dashboard) {
const inDashboardArea = root.inTopPanel(root.panels.dashboardWrapper, root.mouseX, root.mouseY);
if (!inDashboardArea) {
root.dashboardShortcutActive = true;
}
root.screenState.launcher = false;
root.screenState.settings = false;
root.screenState.sidebar = false;
root.screenState.resources = false;
root.popouts.hasCurrent = false;
} else {
root.dashboardShortcutActive = false;
}
}
function onIsDrawingChanged(): void {
if (root.screenState.isDrawing) {
Cursor.setOverride(Qt.BlankCursor);
} else {
Cursor.clearOverride();
}
}
function onLauncherChanged(): void {
if (!root.screenState.launcher) {
root.dashboardShortcutActive = false;
root.osdShortcutActive = false;
root.utilitiesShortcutActive = false;
const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY);
if (!inOsdArea) {
root.screenState.osd = false;
root.panels.osd.hovered = false;
}
}
if (root.screenState.launcher) {
root.screenState.dashboard = false;
root.screenState.dock = false;
root.screenState.settings = false;
}
}
function onOsdChanged(): void {
if (root.screenState.osd) {
const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY);
if (!inOsdArea) {
root.osdShortcutActive = true;
}
} else {
root.osdShortcutActive = false;
}
}
function onResourcesChanged(): void {
if (root.screenState.resources) {
if (root.popouts.currentName.startsWith("audio") || root.popouts.currentName.startsWith("updates"))
root.popouts.hasCurrent = false;
root.screenState.settings = false;
root.screenState.dashboard = false;
}
}
function onSettingsChanged(): void {
if (root.screenState.settings) {
root.screenState.resources = false;
root.screenState.dashboard = false;
root.screenState.sidebar = false;
root.panels.popouts.hasCurrent = false;
root.screenState.launcher = false;
}
}
function onSidebarChanged(): void {
if (root.screenState.sidebar) {
if (root.panels.dashboardWrapper.x + root.panels.dashboardWrapper.width > root.panels.sidebar.x)
root.screenState.dashboard = false;
root.screenState.settings = false;
root.popouts.hasCurrent = false;
}
}
target: root.screenState
}
}
+8 -3
View File
@@ -154,8 +154,10 @@ Item {
property real offsetScale: dashboard.shouldBeActive ? 0 : 1 property real offsetScale: dashboard.shouldBeActive ? 0 : 1
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top anchors.top: parent.top
x: ZUtils.snap((parent.width - implicitWidth) / 2, dashboardWrapper)
clip: offsetScale > 0 clip: offsetScale > 0
implicitHeight: dashboard.implicitHeight * (1 - offsetScale) implicitHeight: dashboard.implicitHeight * (1 - offsetScale)
implicitWidth: dashboard.implicitWidth implicitWidth: dashboard.implicitWidth
@@ -194,8 +196,11 @@ Item {
Settings.Wrapper { Settings.Wrapper {
id: settings id: settings
anchors.centerIn: parent x: ZUtils.snap((parent.width - implicitWidth) / 2, settings)
anchors.verticalCenterOffset: (-implicitHeight - 5 - ((root.height - implicitHeight) / 2)) * offsetScale y: ZUtils.snap((parent.height - implicitHeight) / 2 - (implicitHeight + 5 + (parent.height - implicitHeight) / 2) * offsetScale, settings)
// anchors.centerIn: parent
// anchors.verticalCenterOffset: (-implicitHeight - 5 - ((root.height - implicitHeight) / 2)) * offsetScale
panels: root panels: root
screen: root.screen screen: root.screen
screenState: root.screenState screenState: root.screenState
+24 -2
View File
@@ -56,7 +56,7 @@ CustomWindow {
property color surfaceColor: Colors.tPalette.m3surface property color surfaceColor: Colors.tPalette.m3surface
WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.keyboardFocus: screenState.screenshot ? WlrKeyboardFocus.Exclusive : screenState.launcher || screenState.settings || screenState.sidebar ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None WlrLayershell.keyboardFocus: screenState.screenshot ? WlrKeyboardFocus.Exclusive : screenState.launcher || screenState.settings || screenState.sidebar || screenState.isDrawing ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
WlrLayershell.layer: (fsTransitionProg > 0 && Config.general.showOverFullscreen) || (hasSpecialWorkspace && hasFullscreenOnNormalWs) ? WlrLayer.Overlay : WlrLayer.Top WlrLayershell.layer: (fsTransitionProg > 0 && Config.general.showOverFullscreen) || (hasSpecialWorkspace && hasFullscreenOnNormalWs) ? WlrLayer.Overlay : WlrLayer.Top
color: "transparent" color: "transparent"
mask: screenState.isDrawing || screenState.screenshot ? null : (hasFullscreen && !bar.isHovered ? emptyRegion : regions) mask: screenState.isDrawing || screenState.screenshot ? null : (hasFullscreen && !bar.isHovered ? emptyRegion : regions)
@@ -347,12 +347,31 @@ CustomWindow {
Drawing { Drawing {
id: drawing id: drawing
readonly property int extraWidth: drawingBg.radius * 2
anchors.fill: parent anchors.fill: parent
holeRect: Qt.rect(root.borderThickness + panels.drawing.x - extraWidth, panels.drawing.y + bar.implicitHeight, panels.drawing.implicitWidth + extraWidth, panels.drawing.implicitHeight)
holeRadius: drawingBg.radius
screenState: root.screenState screenState: root.screenState
z: 2 z: 2
} }
Interactions2 { LazyLoader {
id: lazyLoader
activeAsync: root.screenState.isDrawing
component: DrawingInput {
borderThickness: root.borderLayoutThickness
drawing: drawing
panels: panels
screenState: root.screenState
geometry: geometry
parent: root.contentItem
}
}
Interactions {
id: interactions id: interactions
anchors.fill: parent anchors.fill: parent
@@ -365,6 +384,8 @@ CustomWindow {
popouts: panels.popouts popouts: panels.popouts
screen: root.screen screen: root.screen
screenState: root.screenState screenState: root.screenState
input: lazyLoader.item
z: 1
Panels { Panels {
id: panels id: panels
@@ -442,6 +463,7 @@ CustomWindow {
parent: root.contentItem parent: root.contentItem
screen: root.screen screen: root.screen
screenState: root.screenState screenState: root.screenState
z: 4
} }
} }
+105 -43
View File
@@ -5,7 +5,7 @@ import Quickshell
import Quickshell.Io import Quickshell.Io
import QtQuick import QtQuick
import ZShell.Models import ZShell.Models
import ZShell.Internal import ZShell.Wallpaper
import ZShell.Config import ZShell.Config
import qs.Components import qs.Components
import qs.Services import qs.Services
@@ -16,24 +16,48 @@ Searcher {
id: root id: root
property string actualCurrent: WallpaperPath.currentWallpaperPath property string actualCurrent: WallpaperPath.currentWallpaperPath
property alias crops: adapter.monitorCrops
readonly property string current: showPreview ? previewPath : actualCurrent readonly property string current: showPreview ? previewPath : actualCurrent
property alias monitorCrops: monitorCrops readonly property string currentThumbnail: thumbnailFor(current)
readonly property bool currentIsVideo: isVideoPath(current)
property bool pendingPreviewClear property bool pendingPreviewClear
property bool previewColorLock property bool previewColorLock
property string previewPath property string previewPath
property bool recentlyChanged property bool recentlyChanged
property bool showPreview: false property bool showPreview: false
property int cropVersion: 0
function getCategoryFor(w: FileSystemEntry): string { function getCategoryFor(w: WallpaperEntry): string {
let category = w.parentDir.slice(Paths.wallsdir.length + 1); let category = w.parentDir.slice(Paths.wallsdir.length + 1);
if (category.includes("/")) if (category.includes("/"))
category = category.slice(0, category.indexOf("/")); category = category.slice(0, category.indexOf("/"));
return category; return category;
} }
readonly property var videoExts: ["mp4", "mkv", "webm", "avi", "mov", "wmv", "flv", "m4v", "mpg", "mpeg", "3gp", "ogv", "ts", "vob"]
function isVideoPath(path: string): bool {
if (!path)
return false;
const dot = path.lastIndexOf(".");
if (dot < 0)
return false;
return videoExts.includes(path.slice(dot + 1).toLowerCase());
}
function thumbnailFor(path: string): string {
if (!path)
return "";
for (const entry of root.list) {
if (entry.path === path)
return entry.thumbnailPath || path;
}
return path;
}
function getCrop(screen: string): var { function getCrop(screen: string): var {
return root.crops[screen]; return wallpapers.crop(root.current, screen);
} }
function preview(path: string): void { function preview(path: string): void {
@@ -50,19 +74,18 @@ Searcher {
else if (zoom > 5.0) else if (zoom > 5.0)
zoom = 5.0; zoom = 5.0;
root.crops[screen] = { wallpapers.setCrop(root.current, screen, {
x: rect.x, x: rect.x,
y: rect.y, y: rect.y,
width: rect.width, width: rect.width,
height: rect.height, height: rect.height,
zoom: zoom zoom: zoom
}; });
} }
function setWallpaper(path: string): void { function setWallpaper(path: string): void {
actualCurrent = path; actualCurrent = path;
WallpaperPath.currentWallpaperPath = path; WallpaperPath.currentWallpaperPath = path;
Quickshell.screens.forEach(n => setCrop(n.name, Qt.rect(0, 0, 1, 1), 1.0));
Quickshell.execDetached(["zshell-cli", "wallpaper", "lockscreen", "--input-image", `${root.actualCurrent}`, "--output-path", `${Paths.state}/lockscreen_bg.png`, "--blur-amount", `${Config.lock.blurAmount}`]); Quickshell.execDetached(["zshell-cli", "wallpaper", "lockscreen", "--input-image", `${root.actualCurrent}`, "--output-path", `${Paths.state}/lockscreen_bg.png`, "--blur-amount", `${Config.lock.blurAmount}`]);
if (Config.colors.schemeGen) if (Config.colors.schemeGen)
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--image-path", `${root.actualCurrent}`, "--scheme", `${Config.colors.schemeType}`, "--mode", `${Config.colors.mode}`]); Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--image-path", `${root.actualCurrent}`, "--scheme", `${Config.colors.schemeType}`, "--mode", `${Config.colors.mode}`]);
@@ -96,38 +119,13 @@ Searcher {
target: "wallpaper" target: "wallpaper"
} }
FileView { WallpaperDb {
id: monitorCrops
path: `${Paths.state}/wallpaper-crops.json`
watchChanges: true
onAdapterUpdated: cropWriteDelay.restart()
onFileChanged: reload()
JsonAdapter {
id: adapter
property var monitorCrops: ({})
}
}
Timer {
id: cropWriteDelay
interval: 100
repeat: false
running: false
onTriggered: monitorCrops.writeAdapter()
}
FileSystemModel {
id: wallpapers id: wallpapers
filter: FileSystemModel.Images dbPath: `${Paths.state}/wallpapers.sqlite`
cacheSize: wallpaper.largestBufferSize
requireHardwareCodec: true
path: Config.general.wallpaperPath path: Config.general.wallpaperPath
recursive: true
} }
Process { Process {
@@ -146,36 +144,100 @@ Searcher {
WallpaperService { WallpaperService {
id: wallpaper id: wallpaper
dbPath: wallpapers.dbPath
path: root.current path: root.current
} }
Connections {
target: wallpapers
function onCacheStored(path: string): void {
if (path === root.current)
wallpaper.invalidateCache();
}
function onCropsChanged(path: string): void {
if (path === root.current)
root.cropVersion++;
}
}
Variants { Variants {
id: screenVariants
model: Quickshell.screens model: Quickshell.screens
WallpaperObject { WallpaperObject {
readonly property var crops: root.getCrop(modelData.name) id: obj
required property var modelData required property var modelData
cropHeight: crops?.height ?? 1.0 // Crop animation only for edits, never for wallpaper swaps.
cropWidth: crops?.width ?? 1.0 property bool animateCrop: false
cropX: crops?.x ?? 0.0
cropY: crops?.y ?? 0.0
screen: modelData.name screen: modelData.name
service: wallpaper service: wallpaper
zoom: crops?.zoom ?? 1.0
// Apply the wallpaper's own crop. Driven off the service's pathChanged
// (which fires after applyPath has snapshotted the outgoing frame with
// the outgoing crop) and off crop edits, never as a reactive binding on
// the wallpaper path — so a swap applies the incoming crop atomically
// after the base is captured.
function seedCrop(): void {
// Load the current wallpaper's crop without pushing: the service
// already read it from the database. This only seeds the values so
// a later edit animates from the right starting point.
obj.syncCrop(root.getCrop(modelData.name));
}
// Apply an edited crop: assign via the setters so the Behaviors animate
// the numbers and push each intermediate value to the service.
function editCrop(): void {
const c = root.getCrop(modelData.name);
obj.cropX = c?.x ?? 0.0;
obj.cropY = c?.y ?? 0.0;
obj.cropWidth = c?.width ?? 1.0;
obj.cropHeight = c?.height ?? 1.0;
obj.zoom = c?.zoom ?? 1.0;
}
Component.onCompleted: seedCrop()
onScreenChanged: seedCrop()
Behavior on cropHeight { Behavior on cropHeight {
enabled: obj.animateCrop
Anim {} Anim {}
} }
Behavior on cropWidth { Behavior on cropWidth {
enabled: obj.animateCrop
Anim {} Anim {}
} }
Behavior on cropX { Behavior on cropX {
enabled: obj.animateCrop
Anim {} Anim {}
} }
Behavior on cropY { Behavior on cropY {
enabled: obj.animateCrop
Anim {} Anim {}
} }
} }
} }
Connections {
target: wallpaper
function onPathChanged(): void {
for (const obj of screenVariants.instances) {
obj.animateCrop = false;
obj.seedCrop();
}
}
}
Connections {
target: root
function onCropVersionChanged(): void {
for (const obj of screenVariants.instances) {
obj.animateCrop = true;
obj.editCrop();
}
}
}
} }
+3 -3
View File
@@ -8,6 +8,7 @@ import ZShell.Components
import qs.Components import qs.Components
import qs.Modules.Bar.Components import qs.Modules.Bar.Components
import qs.Modules.Bar.Popouts import qs.Modules.Bar.Popouts
import ZShell
SnapLayout { SnapLayout {
id: root id: root
@@ -23,7 +24,7 @@ SnapLayout {
function axisCenterOf(item: Item): real { function axisCenterOf(item: Item): real {
const c = item.mapToItem(root, item.implicitWidth / 2, item.implicitHeight / 2); const c = item.mapToItem(root, item.implicitWidth / 2, item.implicitHeight / 2);
return horizontal ? Math.floor(c.x) : Math.floor(c.y); return horizontal ? ZUtils.snap(c.x, item) : ZUtils.snap(c.y, item);
} }
function checkPopout(pos: real): void { function checkPopout(pos: real): void {
@@ -249,8 +250,7 @@ SnapLayout {
roleValue: "network" roleValue: "network"
delegate: EntryWrapper { delegate: EntryWrapper {
NetworkWidget { NetworkWidget {}
}
} }
} }
+3 -1
View File
@@ -46,7 +46,9 @@ WidgetBase {
} }
required property SnapLayout loader required property SnapLayout loader
required property Wrapper popouts required property Wrapper popouts
readonly property real size: horizontal ? timeText.contentWidth + Tokens.padding.medium * 2 : verticalColumn.implicitHeight + Tokens.padding.medium * 2 readonly property int roundedWidth: Math.round(timeText.implicitWidth)
readonly property int evenWidth: roundedWidth % 2 === 0 ? roundedWidth : roundedWidth + 1
readonly property real size: horizontal ? evenWidth + Tokens.padding.medium * 2 : verticalColumn.implicitHeight + Tokens.padding.medium * 2
required property ScreenState screenState required property ScreenState screenState
implicitHeight: horizontal ? baseSize : size implicitHeight: horizontal ? baseSize : size
@@ -24,7 +24,7 @@ Row {
color: Battery.getColors(root.percentage, root.devState).bg color: Battery.getColors(root.percentage, root.devState).bg
height: root.batHeight height: root.batHeight
radius: root.radius radius: root.radius
width: root.batWidth width: root.batWidth - 1
CustomText { CustomText {
color: Battery.getColors(root.percentage, root.devState).fg color: Battery.getColors(root.percentage, root.devState).fg
+1 -1
View File
@@ -14,7 +14,7 @@ WidgetBase {
readonly property int textWidth: Math.min(metrics.width, 200) readonly property int textWidth: Math.min(metrics.width, 200)
implicitHeight: horizontal ? baseSize : layout.implicitHeight + Tokens.padding.small * 2 implicitHeight: horizontal ? baseSize : layout.implicitHeight + Tokens.padding.small * 2
implicitWidth: horizontal ? layout.implicitWidth + Tokens.padding.small * 2 : baseSize implicitWidth: horizontal ? (layout.implicitWidth % 2 === 0 ? layout.implicitWidth : layout.implicitWidth + 1) + Tokens.padding.small * 2 : baseSize
radius: Tokens.rounding.full radius: Tokens.rounding.full
Behavior on implicitHeight { Behavior on implicitHeight {
+8 -12
View File
@@ -13,7 +13,6 @@ GridLayout {
property real animatedPercentage: 0 property real animatedPercentage: 0
readonly property real arcStartAngle: 0.75 * Math.PI readonly property real arcStartAngle: 0.75 * Math.PI
readonly property real arcSweep: 1.5 * Math.PI readonly property real arcSweep: 1.5 * Math.PI
readonly property real dpr: (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1
required property bool horizontal required property bool horizontal
property string icon property string icon
required property color iconColor required property color iconColor
@@ -24,17 +23,12 @@ GridLayout {
property bool warning: percentage * 100 >= warningThreshold property bool warning: percentage * 100 >= warningThreshold
property int warningThreshold: 80 property int warningThreshold: 80
function snap(v) {
return Math.round(v * dpr) / dpr;
}
columnSpacing: Tokens.spacing.medium columnSpacing: Tokens.spacing.medium
columns: horizontal ? -1 : 1 columns: horizontal ? -1 : 1
percentage: 0 percentage: 0
Behavior on animatedPercentage { Behavior on animatedPercentage {
Anim { Anim {}
}
} }
Component.onCompleted: animatedPercentage = percentage Component.onCompleted: animatedPercentage = percentage
@@ -60,7 +54,7 @@ GridLayout {
Layout.preferredHeight: root.horizontal ? icon.implicitHeight - Tokens.padding.extraSmall : 4 Layout.preferredHeight: root.horizontal ? icon.implicitHeight - Tokens.padding.extraSmall : 4
Layout.preferredWidth: root.horizontal ? 4 : icon.implicitWidth Layout.preferredWidth: root.horizontal ? 4 : icon.implicitWidth
color: Colors.layer(Colors.palette.m3surfaceContainerHigh, 2) color: Colors.layer(Colors.palette.m3surfaceContainerHigh, 2)
radius: Math.min(width, height) / 2 radius: Tokens.rounding.full
Item { Item {
id: clipper id: clipper
@@ -70,18 +64,20 @@ GridLayout {
anchors.left: root.horizontal ? undefined : parent.left anchors.left: root.horizontal ? undefined : parent.left
anchors.verticalCenter: root.horizontal ? undefined : parent.verticalCenter anchors.verticalCenter: root.horizontal ? undefined : parent.verticalCenter
clip: true clip: true
height: root.horizontal ? root.snap(root.percentage * track.height) : track.height * 2 implicitHeight: root.horizontal ? root.percentage * track.height : track.height * 2
width: root.horizontal ? track.width * 2 : root.snap(root.percentage * track.width) implicitWidth: root.horizontal ? track.width * 2 : root.percentage * track.width
CustomRect { CustomRect {
anchors.bottom: root.horizontal ? parent.bottom : undefined anchors.bottom: root.horizontal ? parent.bottom : undefined
anchors.horizontalCenter: root.horizontal ? parent.horizontalCenter : undefined anchors.horizontalCenter: root.horizontal ? parent.horizontalCenter : undefined
anchors.left: root.horizontal ? undefined : parent.left anchors.left: root.horizontal ? undefined : parent.left
anchors.verticalCenter: root.horizontal ? undefined : parent.verticalCenter anchors.verticalCenter: root.horizontal ? undefined : parent.verticalCenter
color: root.mainColor color: root.mainColor
height: track.height
radius: track.radius radius: track.radius
width: track.width
implicitHeight: track.height
implicitWidth: track.width
} }
} }
} }
+2
View File
@@ -34,6 +34,7 @@ WidgetBase {
anchors.horizontalCenterOffset: root.horizontal ? 0 : 1 anchors.horizontalCenterOffset: root.horizontal ? 0 : 1
anchors.verticalCenterOffset: root.horizontal ? 0 : -3 anchors.verticalCenterOffset: root.horizontal ? 0 : -3
columnSpacing: Tokens.spacing.small columnSpacing: Tokens.spacing.small
rowSpacing: 0
columns: root.horizontal ? -1 : 1 columns: root.horizontal ? -1 : 1
ServiceRef { ServiceRef {
@@ -52,6 +53,7 @@ WidgetBase {
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
Layout.fillHeight: root.horizontal Layout.fillHeight: root.horizontal
Layout.fillWidth: !root.horizontal Layout.fillWidth: !root.horizontal
horizontal: root.horizontal horizontal: root.horizontal
icon: "memory" icon: "memory"
iconColor: root.screenState.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface iconColor: root.screenState.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
+3 -3
View File
@@ -32,7 +32,7 @@ WidgetBase {
return -1; return -1;
} }
readonly property real size: horizontal ? grid.implicitWidth + grid.anchors.leftMargin + grid.anchors.rightMargin : grid.implicitHeight + grid.anchors.bottomMargin + grid.anchors.topMargin readonly property real size: horizontal ? grid.implicitWidth + grid.anchors.leftMargin + grid.anchors.rightMargin : grid.implicitHeight + grid.anchors.bottomMargin + grid.anchors.topMargin
readonly property int spacing: Tokens.spacing.medium / 2 readonly property int spacing: Tokens.spacing.extraSmall
// Entries that can shrink to nothing, spacing included // Entries that can shrink to nothing, spacing included
function collapsed(entry: var): bool { function collapsed(entry: var): bool {
@@ -134,8 +134,8 @@ WidgetBase {
Layout.rightMargin: root.horizontal ? Math.round(rightGap) : 0 Layout.rightMargin: root.horizontal ? Math.round(rightGap) : 0
Layout.topMargin: root.horizontal ? 0 : Math.round(leftGap) Layout.topMargin: root.horizontal ? 0 : Math.round(leftGap)
children: item children: item
implicitHeight: item?.implicitHeight ?? 0 implicitHeight: item?.height ?? item?.implicitHeight ?? 0
implicitWidth: item?.implicitWidth ?? 0 implicitWidth: item?.width ?? item?.implicitWidth ?? 0
Behavior on leftGap { Behavior on leftGap {
Anim { Anim {
@@ -7,7 +7,7 @@ import qs.Components
import qs.Services import qs.Services
MaterialIcon { MaterialIcon {
id: speaker id: root
required property bool horizontal required property bool horizontal
@@ -16,6 +16,8 @@ MaterialIcon {
fill: 1 fill: 1
fontStyle: Tokens.font.icon.small fontStyle: Tokens.font.icon.small
text: Audio.muted ? "volume_off" : "volume_up" text: Audio.muted ? "volume_off" : "volume_up"
width: Math.round(implicitWidth)
height: Math.round(implicitHeight)
Behavior on Layout.maximumWidth { Behavior on Layout.maximumWidth {
Anim {} Anim {}
@@ -7,7 +7,7 @@ import qs.Components
import qs.Services import qs.Services
MaterialIcon { MaterialIcon {
id: mic id: root
required property bool horizontal required property bool horizontal
@@ -16,6 +16,8 @@ MaterialIcon {
fill: 1 fill: 1
fontStyle: Tokens.font.icon.small fontStyle: Tokens.font.icon.small
text: Audio.sourceMuted ? "mic_off" : "mic" text: Audio.sourceMuted ? "mic_off" : "mic"
width: Math.round(implicitWidth)
height: Math.round(implicitHeight)
Behavior on Layout.maximumWidth { Behavior on Layout.maximumWidth {
Anim {} Anim {}
+25 -27
View File
@@ -12,13 +12,14 @@ import qs.Services
Item { Item {
id: root id: root
property real activeWorkspaceMargin: Math.ceil(Tokens.padding.medium / 4) property real activeWorkspaceMargin: Tokens.padding.extraSmall / 2
readonly property int spacing: Tokens.spacing.extraSmall / 2
readonly property int effectiveActiveWorkspaceId: monitor?.activeWorkspace?.id ?? 1 readonly property int effectiveActiveWorkspaceId: monitor?.activeWorkspace?.id ?? 1
required property bool horizontal required property bool horizontal
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen) readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen)
required property ShellScreen screen required property ShellScreen screen
readonly property real shortSize: Math.max(Config.bar.height, Tokens.sizes.bar.innerSize) readonly property real shortSize: Math.max(Config.bar.height, Tokens.sizes.bar.innerSize)
readonly property real size: (workspaceButtonWidth * workspacesShown) + activeWorkspaceMargin * 2 readonly property real size: (workspaceButtonWidth * workspacesShown) + (spacing * Math.max(0, workspacesShown - 1)) + activeWorkspaceMargin * 2
property int workspaceButtonWidth: (horizontal ? bgRect.implicitHeight : bgRect.implicitWidth) - root.activeWorkspaceMargin * 2 property int workspaceButtonWidth: (horizontal ? bgRect.implicitHeight : bgRect.implicitWidth) - root.activeWorkspaceMargin * 2
property int workspaceIndexInGroup: (effectiveActiveWorkspaceId - 1) % root.workspacesShown property int workspaceIndexInGroup: (effectiveActiveWorkspaceId - 1) % root.workspacesShown
readonly property list<var> workspaces: Hyprland.workspaces.values.filter(w => w.monitor === root.monitor) readonly property list<var> workspaces: Hyprland.workspaces.values.filter(w => w.monitor === root.monitor)
@@ -59,15 +60,17 @@ Item {
anchors.fill: parent anchors.fill: parent
anchors.margins: root.activeWorkspaceMargin anchors.margins: root.activeWorkspaceMargin
columnSpacing: 0 columnSpacing: root.spacing
columns: root.horizontal ? root.workspacesShown : 1 columns: root.horizontal ? root.workspacesShown : 1
rowSpacing: 0 rowSpacing: root.spacing
rows: root.horizontal ? 1 : root.workspacesShown rows: root.horizontal ? 1 : root.workspacesShown
layer.enabled: true
layer.mipmap: true
Repeater { Repeater {
model: root.workspaces model: root.workspaces
Button { CustomRect {
id: button id: button
required property int index required property int index
@@ -75,27 +78,19 @@ Item {
implicitHeight: indicator.indicatorThickness implicitHeight: indicator.indicatorThickness
implicitWidth: indicator.indicatorThickness implicitWidth: indicator.indicatorThickness
width: root.workspaceButtonWidth radius: Tokens.rounding.full
background: Item { CustomText {
id: workspaceButtonBackground anchors.centerIn: parent
color: Colors.palette.m3onSecondaryContainer
implicitHeight: root.workspaceButtonWidth text: button.modelData?.name ?? ""
implicitWidth: root.workspaceButtonWidth
CustomText {
anchors.centerIn: parent
color: Colors.palette.m3onSecondaryContainer
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
text: button.modelData?.name ?? ""
verticalAlignment: Text.AlignVCenter
}
} }
onPressed: { StateLayer {
const ws = button.modelData.name; onPressed: {
Hyprland.dispatch(Hyprland.usingLua ? `hl.dsp.focus({ workspace= "${ws}"})` : `workspace ${ws}`); const ws = button.modelData.name;
Hyprland.dispatch(Hyprland.usingLua ? `hl.dsp.focus({ workspace= "${ws}"})` : `workspace ${ws}`);
}
} }
} }
} }
@@ -104,8 +99,10 @@ Item {
CustomRect { CustomRect {
id: indicator id: indicator
property real indicatorLength: (Math.abs(idxPair.idx1 - idxPair.idx2) + 1) * root.workspaceButtonWidth readonly property real step: root.workspaceButtonWidth + root.spacing
property real indicatorPosition: Math.min(idxPair.idx1, idxPair.idx2) * root.workspaceButtonWidth + root.activeWorkspaceMargin
property real indicatorLength: (Math.abs(idxPair.idx1 - idxPair.idx2) + 1) * root.workspaceButtonWidth + Math.abs(idxPair.idx1 - idxPair.idx2) * root.spacing
property real indicatorPosition: Math.min(idxPair.idx1, idxPair.idx2) * step + root.activeWorkspaceMargin
property real indicatorThickness: root.workspaceButtonWidth property real indicatorThickness: root.workspaceButtonWidth
anchors.horizontalCenter: root.horizontal ? undefined : parent.horizontalCenter anchors.horizontalCenter: root.horizontal ? undefined : parent.horizontalCenter
@@ -129,9 +126,10 @@ Item {
implicitHeight: grid.height implicitHeight: grid.height
implicitWidth: grid.width implicitWidth: grid.width
source: grid source: grid
autoPaddingEnabled: false
sourceColor: Colors.palette.m3onSurface sourceColor: Colors.palette.m3onSurface
x: root.horizontal ? -indicator.x + 3 : 0 x: root.horizontal ? -indicator.x + root.activeWorkspaceMargin : 0
y: root.horizontal ? 0 : -indicator.y + 3 y: root.horizontal ? 0 : -indicator.y + root.activeWorkspaceMargin
} }
} }
} }
+4 -4
View File
@@ -4,6 +4,7 @@ import QtQuick
import Quickshell import Quickshell
import qs.Components import qs.Components
import ZShell.Config import ZShell.Config
import ZShell
Item { Item {
id: root id: root
@@ -36,7 +37,7 @@ Item {
const diff = parent.width - Math.floor(off + content.nonAnimWidth); const diff = parent.width - Math.floor(off + content.nonAnimWidth);
if (diff < 0) if (diff < 0)
return off + diff; return off + diff;
return Math.floor(Math.max(off, 0)); return ZUtils.snap(Math.max(off, 0), root);
} }
y: { y: {
if (horizontal) if (horizontal)
@@ -46,12 +47,11 @@ Item {
const diff = parent.height - Math.floor(off + content.nonAnimHeight); const diff = parent.height - Math.floor(off + content.nonAnimHeight);
if (diff < 0) if (diff < 0)
return off + diff; return off + diff;
return Math.max(off, 0); return ZUtils.snap(Math.max(off, 0), root);
} }
Behavior on offsetScale { Behavior on offsetScale {
Anim { Anim {}
}
} }
Behavior on x { Behavior on x {
enabled: !root.horizontal || root.offsetScale < 1 enabled: !root.horizontal || root.offsetScale < 1
+3 -2
View File
@@ -2,6 +2,7 @@ import Quickshell
import QtQuick import QtQuick
import qs.Components import qs.Components
import ZShell.Config import ZShell.Config
import ZShell
Item { Item {
id: root id: root
@@ -26,8 +27,8 @@ Item {
detachedMode = ""; detachedMode = "";
} }
implicitHeight: nonAnimHeight implicitHeight: ZUtils.snap(nonAnimHeight, root)
implicitWidth: nonAnimWidth implicitWidth: ZUtils.snap(nonAnimWidth, root)
Behavior on implicitHeight { Behavior on implicitHeight {
enabled: root.offsetScale < 1 enabled: root.offsetScale < 1
+4 -2
View File
@@ -55,7 +55,7 @@ Item {
} }
} }
ClipRect { CustomRect {
id: preview id: preview
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
@@ -65,6 +65,7 @@ Item {
color: Colors.tPalette.m3surfaceContainer color: Colors.tPalette.m3surfaceContainer
implicitWidth: ClipHistory.previewIsImage ? Math.max(Math.min(imagePreview.sourceSize.width + Tokens.padding.large * 2, Config.clipboard.sizes.previewWidth), Config.clipboard.sizes.minPreviewWidth) : Math.max(Math.min(textPreviewColumn.width + textPreviewColumn.anchors.margins * 2, Config.clipboard.sizes.previewWidth), Config.clipboard.sizes.minPreviewWidth) implicitWidth: ClipHistory.previewIsImage ? Math.max(Math.min(imagePreview.sourceSize.width + Tokens.padding.large * 2, Config.clipboard.sizes.previewWidth), Config.clipboard.sizes.minPreviewWidth) : Math.max(Math.min(textPreviewColumn.width + textPreviewColumn.anchors.margins * 2, Config.clipboard.sizes.previewWidth), Config.clipboard.sizes.minPreviewWidth)
radius: 25 radius: 25
clip: true
Behavior on implicitWidth { Behavior on implicitWidth {
Anim {} Anim {}
@@ -229,11 +230,12 @@ Item {
anchors.fill: parent anchors.fill: parent
spacing: Tokens.spacing.small spacing: Tokens.spacing.small
ClipRect { Item {
id: textRect id: textRect
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
clip: true
Item { Item {
id: textWrapper id: textWrapper
+4 -4
View File
@@ -17,7 +17,7 @@ Item {
implicitWidth: btn.implicitWidth * 0.9 implicitWidth: btn.implicitWidth * 0.9
implicitHeight: btn.implicitHeight * 0.9 implicitHeight: btn.implicitHeight * 0.9
BlobGroup { BlobGroup2 {
id: blobGroup id: blobGroup
color: Colors.palette.m3surfaceContainerHighest color: Colors.palette.m3surfaceContainerHighest
@@ -29,7 +29,7 @@ Item {
} }
} }
BlobRect { BlobRect2 {
id: btnRect id: btnRect
anchors.fill: parent anchors.fill: parent
@@ -42,7 +42,7 @@ Item {
} }
} }
BlobRect { BlobRect2 {
id: rect id: rect
anchors.right: parent.right anchors.right: parent.right
@@ -143,8 +143,8 @@ Item {
] ]
SnapLayout { SnapLayout {
orientation: Qt.Vertical
id: layout id: layout
orientation: Qt.Vertical
anchors.centerIn: parent anchors.centerIn: parent
spacing: Tokens.spacing.extraSmall spacing: Tokens.spacing.extraSmall
+60 -8
View File
@@ -1,3 +1,5 @@
pragma ComponentBehavior: Bound
import Quickshell import Quickshell
import QtQuick import QtQuick
import ZShell.Config import ZShell.Config
@@ -9,7 +11,7 @@ Item {
id: root id: root
required property bool isCurrent required property bool isCurrent
required property var modelData // FileSystemEntry-like {path, relativePath} required property var modelData
required property real tileHeight required property real tileHeight
required property real tileWidth required property real tileWidth
required property ScreenState screenState required property ScreenState screenState
@@ -29,25 +31,75 @@ Item {
radius: Tokens.rounding.small radius: Tokens.rounding.small
y: Tokens.padding.large y: Tokens.padding.large
MaterialIcon { Loader {
active: opacity > 0
anchors.centerIn: parent anchors.centerIn: parent
color: Colors.tPalette.m3outline opacity: img.status === Image.Ready ? 0 : 1
fontStyle: Tokens.font.icon.builders.extraLarge.scale(2).weight(Font.DemiBold).build()
text: "image" Behavior on opacity {
Anim {}
}
sourceComponent: CustomRect {
color: Colors.palette.m3primaryContainer
implicitHeight: loadingIndicator.implicitSize + Tokens.padding.large * 2
implicitWidth: loadingIndicator.implicitSize + Tokens.padding.large * 2
radius: Tokens.rounding.full
LoadingIndicator {
id: loadingIndicator
anchors.centerIn: parent
containsIcon: true
implicitSize: Math.min(image.width, image.height) * 0.3
}
}
} }
CachingImage { Image {
id: img
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top anchors.top: parent.top
cache: true asynchronous: true
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
height: root.tileHeight height: root.tileHeight
path: root.modelData ? root.modelData.path : "" source: root.modelData ? root.modelData.thumbnailPath : ""
smooth: root.isCurrent smooth: root.isCurrent
sourceSize.height: root.tileHeight sourceSize.height: root.tileHeight
sourceSize.width: root.tileHeight * (16 / 9) sourceSize.width: root.tileHeight * (16 / 9)
width: root.tileHeight * (16 / 9) width: root.tileHeight * (16 / 9)
opacity: status === Image.Ready ? 1 : 0
Behavior on opacity {
Anim {
type: Anim.SlowEffects
}
}
Loader {
opacity: root.modelData.isVideo && img.status === Image.Ready ? 1 : 0
active: opacity > 0
anchors.centerIn: parent
Behavior on opacity {
Anim {
type: Anim.SlowEffects
}
}
sourceComponent: CustomRect {
implicitWidth: videoIcon.implicitWidth + Tokens.padding.extraLarge * 2
implicitHeight: implicitWidth
radius: Tokens.rounding.full
color: Colors.tPalette.m3surface
MaterialIcon {
id: videoIcon
anchors.centerIn: parent
text: "video_library"
fontStyle: Tokens.font.icon.extraLarge
}
}
}
} }
StateLayer { StateLayer {
+4 -19
View File
@@ -6,14 +6,13 @@ import qs.Components
import qs.Helpers import qs.Helpers
import qs.Services import qs.Services
ClipRect { Item {
id: root id: root
required property var lock required property var lock
implicitHeight: layout.implicitHeight implicitHeight: layout.implicitHeight
implicitWidth: layout.implicitWidth implicitWidth: layout.implicitWidth
radius: Tokens.rounding.largeIncreased
SnapLayout { SnapLayout {
id: layout id: layout
@@ -54,24 +53,10 @@ ClipRect {
lock: root.lock lock: root.lock
} }
SnapLayout { NotifDock {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true
spacing: Tokens.spacing.medium lock: root.lock
orientation: Qt.Vertical
CustomRect {
Layout.fillHeight: true
Layout.fillWidth: true
bottomRightRadius: Tokens.rounding.largeIncreased
color: Colors.tPalette.m3surfaceContainer
radius: Tokens.rounding.small
topRightRadius: Tokens.rounding.largeIncreased
NotifDock {
lock: root.lock
}
}
} }
} }
} }
+1 -1
View File
@@ -221,9 +221,9 @@ WlSessionLockSurface {
anchors.fill: parent anchors.fill: parent
color: Colors.palette.m3surface color: Colors.palette.m3surface
layer.enabled: true
opacity: Colors.transparency.enabled ? Colors.transparency.base : 1 opacity: Colors.transparency.enabled ? Colors.transparency.base : 1
radius: lockContent.radius radius: lockContent.radius
layer.enabled: true
layer.effect: MultiEffect { layer.effect: MultiEffect {
blurMax: 15 blurMax: 15
+31 -26
View File
@@ -7,39 +7,44 @@ import qs.Components
import qs.Helpers import qs.Helpers
import qs.Services import qs.Services
ClipRect { Item {
id: root id: root
required property var lock required property var lock
color: Colors.tPalette.m3surfaceContainer
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2 implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
radius: Tokens.rounding.small
FadeImage {
id: image
ClipRect {
anchors.fill: parent anchors.fill: parent
asynchronous: true
fillMode: Image.PreserveAspectCrop
layer.enabled: true
opacity: status === Image.Ready ? 1 : 0
source: Players.getArtUrl(Players.active)
sourceSize: {
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
return Qt.size(width * dpr, height * dpr);
}
Behavior on opacity { color: Colors.tPalette.m3surfaceContainer
Anim { radius: Tokens.rounding.small
type: Anim.StandardExtraLarge bottomLeftRadius: Tokens.rounding.largeIncreased
}
} FadeImage {
id: image
CustomRect {
anchors.fill: parent anchors.fill: parent
color: Colors.palette.m3surface asynchronous: true
opacity: 0.7 fillMode: Image.PreserveAspectCrop
opacity: status === Image.Ready ? 1 : 0
source: Players.getArtUrl(Players.active)
sourceSize: {
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
return Qt.size(width * dpr, height * dpr);
}
Behavior on opacity {
Anim {
type: Anim.StandardExtraLarge
}
}
CustomRect {
anchors.fill: parent
color: Colors.palette.m3surface
opacity: 0.7
}
} }
} }
@@ -79,7 +84,7 @@ ClipRect {
spacing: Tokens.spacing.extraSmall spacing: Tokens.spacing.extraSmall
IconButton { IconButton {
enabled: Players.active?.canGoPrevious enabled: Players.active?.canGoPrevious ?? false
icon: "skip_previous" icon: "skip_previous"
isRound: true isRound: true
shapeMorph: true shapeMorph: true
@@ -90,7 +95,7 @@ ClipRect {
IconButton { IconButton {
checked: Players.active?.isPlaying ?? false checked: Players.active?.isPlaying ?? false
enabled: Players.active?.canTogglePlaying enabled: Players.active?.canTogglePlaying ?? false
icon: Players.active?.isPlaying ? "pause" : "play_arrow" icon: Players.active?.isPlaying ? "pause" : "play_arrow"
implicitWidth: implicitHeight + Tokens.padding.largeIncreased * 2 implicitWidth: implicitHeight + Tokens.padding.largeIncreased * 2
isRound: true isRound: true
@@ -100,7 +105,7 @@ ClipRect {
} }
IconButton { IconButton {
enabled: Players.active?.canGoNext enabled: Players.active?.canGoNext ?? false
icon: "skip_next" icon: "skip_next"
isRound: true isRound: true
shapeMorph: true shapeMorph: true
+80 -96
View File
@@ -12,129 +12,113 @@ import qs.Helpers
import qs.Daemons import qs.Daemons
import qs.Services import qs.Services
SnapLayout { CustomRect {
id: root id: root
required property var lock required property var lock
anchors.fill: parent bottomRightRadius: Tokens.rounding.largeIncreased
anchors.margins: Tokens.padding.large color: Colors.tPalette.m3surfaceContainer
radius: Tokens.rounding.small
topRightRadius: Tokens.rounding.largeIncreased
orientation: Qt.Vertical SnapLayout {
spacing: Tokens.spacing.small anchors.fill: parent
anchors.margins: Tokens.padding.large
CustomText { orientation: Qt.Vertical
Layout.fillWidth: true spacing: Tokens.spacing.small
color: Colors.palette.m3outline
elide: Text.ElideRight
font: Tokens.font.mono.builders.small.weight(Font.Medium).build()
text: NotifServer.list.length > 0 ? qsTr("%1 notification%2").arg(NotifServer.list.length).arg(NotifServer.list.length === 1 ? "" : "s") : qsTr("Notifications")
}
ClipRect { CustomText {
id: clipRect Layout.fillWidth: true
color: Colors.palette.m3outline
elide: Text.ElideRight
font: Tokens.font.mono.builders.small.weight(Font.Medium).build()
text: NotifServer.list.length > 0 ? qsTr("%1 notification%2").arg(NotifServer.list.length).arg(NotifServer.list.length === 1 ? "" : "s") : qsTr("Notifications")
}
Layout.fillHeight: true Item {
Layout.fillWidth: true Layout.fillHeight: true
color: "transparent" Layout.fillWidth: true
radius: Tokens.rounding.small
Loader { Loader {
active: opacity > 0 active: opacity > 0
anchors.centerIn: parent anchors.centerIn: parent
opacity: NotifServer.list.length > 0 ? 0 : 1 opacity: NotifServer.list.length > 0 ? 0 : 1
Behavior on opacity { Behavior on opacity {
Anim { Anim {
duration: Tokens.anim.durations.extraLarge duration: Tokens.anim.durations.extraLarge
}
}
sourceComponent: SnapLayout {
spacing: Tokens.spacing.largeIncreased
orientation: Qt.Vertical
Image {
asynchronous: true
fillMode: Image.PreserveAspectFit
layer.enabled: true
source: Qt.resolvedUrl(`${Quickshell.shellDir}/assets/dino.png`)
sourceSize.width: clipRect.width * 0.8
layer.effect: Coloriser {
brightness: 1
colorizationColor: Colors.palette.m3outlineVariant
} }
} }
sourceComponent: CustomText {
CustomText {
Layout.alignment: Qt.AlignHCenter
color: Colors.palette.m3outlineVariant color: Colors.palette.m3outlineVariant
font: Tokens.font.headline.builders.small.width(90).build() font: Tokens.font.headline.builders.small.width(90).build()
text: qsTr("No Notifications") text: qsTr("No Notifications")
} }
} }
}
CustomListView { CustomListView {
anchors.fill: parent anchors.fill: parent
clip: true clip: true
spacing: Tokens.spacing.small spacing: Tokens.spacing.small
add: Transition { add: Transition {
Anim { Anim {
from: 0 from: 0
property: "opacity" property: "opacity"
to: 1 to: 1
} }
Anim { Anim {
duration: Tokens.anim.durations.expressiveDefaultSpatial duration: Tokens.anim.durations.expressiveDefaultSpatial
easing: Tokens.anim.expressiveDefaultSpatial easing: Tokens.anim.expressiveDefaultSpatial
from: 0 from: 0
property: "scale" property: "scale"
to: 1 to: 1
} }
}
delegate: NotifGroup {}
displaced: Transition {
Anim {
properties: "opacity,scale"
to: 1
} }
delegate: NotifGroup {}
displaced: Transition {
Anim {
properties: "opacity,scale"
to: 1
}
Anim { Anim {
duration: Tokens.anim.durations.expressiveDefaultSpatial duration: Tokens.anim.durations.expressiveDefaultSpatial
easing: Tokens.anim.expressiveDefaultSpatial easing: Tokens.anim.expressiveDefaultSpatial
property: "y" property: "y"
}
} }
} model: ScriptModel {
model: ScriptModel { values: {
values: { const list = NotifServer.notClosed.map(n => [n.appName, null]);
const list = NotifServer.notClosed.map(n => [n.appName, null]); return [...new Map(list).keys()];
return [...new Map(list).keys()]; }
}
}
move: Transition {
Anim {
properties: "opacity,scale"
to: 1
} }
move: Transition {
Anim {
properties: "opacity,scale"
to: 1
}
Anim { Anim {
duration: Tokens.anim.durations.expressiveDefaultSpatial duration: Tokens.anim.durations.expressiveDefaultSpatial
easing: Tokens.anim.expressiveDefaultSpatial easing: Tokens.anim.expressiveDefaultSpatial
property: "y" property: "y"
} }
}
remove: Transition {
Anim {
property: "opacity"
to: 0
} }
remove: Transition {
Anim {
property: "opacity"
to: 0
}
Anim { Anim {
property: "scale" property: "scale"
to: 0.6 to: 0.6
}
} }
} }
} }
+1 -1
View File
@@ -286,7 +286,7 @@ CustomRect {
component NotifLine: CustomText { component NotifLine: CustomText {
id: notifLine id: notifLine
required property NotifServer.Notif modelData required property NotificationRow modelData
Layout.fillWidth: true Layout.fillWidth: true
color: root.urgency === "critical" ? Colors.palette.m3onSecondaryContainer : Colors.palette.m3onSurface color: root.urgency === "critical" ? Colors.palette.m3onSecondaryContainer : Colors.palette.m3onSurface
+4 -3
View File
@@ -18,7 +18,7 @@ orientation: Qt.Vertical
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
animate: true animate: true
color: Colors.palette.m3onSurfaceVariant color: Colors.palette.m3onSurfaceVariant
font: Tokens.font.title.large font: Tokens.font.body.large
text: Weather.description text: Weather.description
} }
@@ -31,7 +31,7 @@ orientation: Qt.Vertical
animate: true animate: true
color: Colors.palette.m3primary color: Colors.palette.m3primary
font: Tokens.font.body.large font: Tokens.font.title.large
text: Weather.temp text: Weather.temp
} }
@@ -39,6 +39,7 @@ orientation: Qt.Vertical
animate: true animate: true
color: Colors.palette.m3secondary color: Colors.palette.m3secondary
text: Weather.icon text: Weather.icon
fontStyle: Tokens.font.icon.large
} }
} }
@@ -55,7 +56,7 @@ orientation: Qt.Vertical
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
animate: true animate: true
color: Colors.palette.m3onSurfaceVariant color: Colors.palette.m3onSurfaceVariant
font: Tokens.font.body.normal font: Tokens.font.body.medium
text: { text: {
const today = Weather.forecast[0]; const today = Weather.forecast[0];
return qsTr("High %1 • Low %2").arg(Weather.formatTemp(today?.maxTempC)).arg(Weather.formatTemp(today?.minTempC)); return qsTr("High %1 • Low %2").arg(Weather.formatTemp(today?.maxTempC)).arg(Weather.formatTemp(today?.minTempC));
+12 -5
View File
@@ -37,7 +37,7 @@ CustomRect {
} }
} }
VerticalFadeListView { CustomListView {
id: layout id: layout
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
@@ -46,7 +46,13 @@ CustomRect {
anchors.margins: Tokens.padding.large anchors.margins: Tokens.padding.large
anchors.right: parent.right anchors.right: parent.right
implicitHeight: contentItem.childrenRect.height implicitHeight: contentItem.childrenRect.height
model: Weather.hourlyForecast currentIndex: Weather.currentHourIndex
// highlightFollowsCurrentItem: true
// preferredHighlightEnd: currentItem.implicitWidth
// preferredHighlightBegin: 0
// highlightRangeMode: ListView.ApplyRange
clip: true
model: Weather.hourlyForecast.slice(Weather.currentHourIndex - 1)
orientation: VerticalFadeListView.Horizontal orientation: VerticalFadeListView.Horizontal
spacing: Tokens.spacing.medium spacing: Tokens.spacing.medium
@@ -54,6 +60,7 @@ CustomRect {
orientation: Qt.Vertical orientation: Qt.Vertical
id: hour id: hour
readonly property bool current: ListView.view.currentItem === this
readonly property var cond: modelData readonly property var cond: modelData
required property int index required property int index
required property var modelData required property var modelData
@@ -62,7 +69,7 @@ orientation: Qt.Vertical
MaterialShape { MaterialShape {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
color: Qt.alpha(Colors.palette.m3primary, hour.index === Weather.currentHourIndex ? 1 : 0) color: Qt.alpha(Colors.palette.m3primary, hour.current ? 1 : 0)
implicitSize: temp.implicitHeight + Tokens.padding.small * 2 implicitSize: temp.implicitHeight + Tokens.padding.small * 2
shape: MaterialShape.Cookie4Sided shape: MaterialShape.Cookie4Sided
@@ -74,7 +81,7 @@ orientation: Qt.Vertical
id: temp id: temp
anchors.centerIn: parent anchors.centerIn: parent
color: hour.index === Weather.currentHourIndex ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface color: hour.current ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
font: Tokens.font.body.medium font: Tokens.font.body.medium
text: Weather.formatTemp(hour.cond.tempC).slice(0, -1) // Remove C/F text: Weather.formatTemp(hour.cond.tempC).slice(0, -1) // Remove C/F
} }
@@ -98,7 +105,7 @@ orientation: Qt.Vertical
Layout.topMargin: Tokens.spacing.extraSmall Layout.topMargin: Tokens.spacing.extraSmall
color: Colors.palette.m3onSurfaceVariant color: Colors.palette.m3onSurfaceVariant
font: Tokens.font.body.small font: Tokens.font.body.small
text: hour.index === 0 ? qsTr("Now") : Qt.formatDateTime(new Date(hour.cond.timestamp.replace("T", " ")), Config.services.useTwelveHourClock ? "ha" : "hh:00") text: hour.current ? qsTr("Now") : Qt.formatDateTime(new Date(hour.cond.timestamp.replace("T", " ")), Config.services.useTwelveHourClock ? "ha" : "hh:00")
} }
} }
} }
+2 -2
View File
@@ -19,6 +19,7 @@ CustomRect {
return base + brief.anchors.topMargin; return base + brief.anchors.topMargin;
} }
radius: Tokens.rounding.small radius: Tokens.rounding.small
topLeftRadius: Tokens.rounding.largeIncreased
Timer { Timer {
interval: 900000 // 15 minutes interval: 900000 // 15 minutes
@@ -48,7 +49,6 @@ CustomRect {
anchors.right: parent.right anchors.right: parent.right
asynchronous: true asynchronous: true
sourceComponent: Forecast { sourceComponent: Forecast {}
}
} }
} }
+2 -1
View File
@@ -5,6 +5,7 @@ import Quickshell.Widgets
import QtQuick import QtQuick
import ZShell.Config import ZShell.Config
import ZShell.Components import ZShell.Components
import ZShell.Services
import qs.Components import qs.Components
import qs.Daemons import qs.Daemons
import qs.Helpers import qs.Helpers
@@ -97,7 +98,7 @@ Item {
property int idx property int idx
required property int index required property int index
required property NotifServer.Notif modelData required property NotificationRow modelData
readonly property alias nonAnimHeight: notif.nonAnimHeight readonly property alias nonAnimHeight: notif.nonAnimHeight
implicitHeight: notif.implicitHeight + (idx === 0 ? 0 : Tokens.spacing.small) implicitHeight: notif.implicitHeight + (idx === 0 ? 0 : Tokens.spacing.small)
+13 -8
View File
@@ -5,7 +5,7 @@ import QtQuick.Shapes
import Quickshell import Quickshell
import Quickshell.Services.Notifications import Quickshell.Services.Notifications
import ZShell.Components import ZShell.Components
import qs.Daemons import ZShell.Services
import qs.Helpers import qs.Helpers
import ZShell.Config import ZShell.Config
import qs.Components import qs.Components
@@ -17,7 +17,7 @@ CustomRect {
property bool expanded: Config.notifs.openExpanded property bool expanded: Config.notifs.openExpanded
readonly property bool hasAppIcon: modelData.appIcon.length > 0 readonly property bool hasAppIcon: modelData.appIcon.length > 0
readonly property bool hasImage: modelData.image.length > 0 readonly property bool hasImage: modelData.image.length > 0
required property NotifServer.Notif modelData required property NotificationRow modelData
readonly property int nonAnimHeight: summary.implicitHeight + (root.expanded ? Tokens.spacing.extraSmall * 2 + appName.height + body.height + actions.height + actions.anchors.topMargin : bodyPreview.height) + inner.anchors.margins * 2 readonly property int nonAnimHeight: summary.implicitHeight + (root.expanded ? Tokens.spacing.extraSmall * 2 + appName.height + body.height + actions.height + actions.anchors.topMargin : bodyPreview.height) + inner.anchors.margins * 2
color: root.modelData.urgency === NotificationUrgency.Critical ? Colors.palette.m3secondaryContainer : Colors.tPalette.m3surfaceContainer color: root.modelData.urgency === NotificationUrgency.Critical ? Colors.palette.m3secondaryContainer : Colors.tPalette.m3surfaceContainer
@@ -56,10 +56,10 @@ CustomRect {
if (actions.length === 1) if (actions.length === 1)
actions[0].invoke(); actions[0].invoke();
} }
onEntered: root.modelData.timer.stop() onEntered: root.modelData.pausePopup()
onExited: { onExited: {
if (!pressed) if (!pressed)
root.modelData.timer.start(); root.modelData.resumePopup();
} }
onPositionChanged: event => { onPositionChanged: event => {
if (pressed) { if (pressed) {
@@ -69,14 +69,14 @@ CustomRect {
} }
} }
onPressed: event => { onPressed: event => {
root.modelData.timer.stop(); root.modelData.pausePopup();
startY = event.y; startY = event.y;
if (event.button === Qt.MiddleButton) if (event.button === Qt.MiddleButton)
root.modelData.close(); root.modelData.close();
} }
onReleased: event => { onReleased: event => {
if (!containsMouse) if (!containsMouse)
root.modelData.timer.start(); root.modelData.resumePopup();
if (Math.abs(root.x) < root.implicitWidth * Config.notifs.clearThreshold) if (Math.abs(root.x) < root.implicitWidth * Config.notifs.clearThreshold)
root.x = 0; root.x = 0;
@@ -200,7 +200,7 @@ CustomRect {
radiusX: progressIndicator.width / 2 - Tokens.padding.extraSmall / 2 radiusX: progressIndicator.width / 2 - Tokens.padding.extraSmall / 2
radiusY: progressIndicator.height / 2 - Tokens.padding.extraSmall / 2 radiusY: progressIndicator.height / 2 - Tokens.padding.extraSmall / 2
startAngle: -90 startAngle: -90
sweepAngle: ((root.modelData.hints.value ?? 0) / 100) * 360 sweepAngle: (root.modelData.progress / 100) * 360
Behavior on sweepAngle { Behavior on sweepAngle {
Anim { Anim {
@@ -477,7 +477,12 @@ CustomRect {
shapeMorph: true shapeMorph: true
text: modelData.text text: modelData.text
onClicked: modelData.invoke() onClicked: {
if (modelData.invokable)
modelData.invoke();
else if (!root.modelData.resident)
root.modelData.close();
}
} }
} }
+1 -1
View File
@@ -9,7 +9,6 @@ import ZShell.Config
import qs.Services import qs.Services
SnapLayout { SnapLayout {
orientation: Qt.Vertical
id: root id: root
property bool animateScroll: false property bool animateScroll: false
@@ -69,6 +68,7 @@ orientation: Qt.Vertical
} }
spacing: Tokens.spacing.largeIncreased spacing: Tokens.spacing.largeIncreased
orientation: Qt.Vertical
Component.onCompleted: applySearchAnchor() Component.onCompleted: applySearchAnchor()
+2 -1
View File
@@ -1,5 +1,6 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Components
import ZShell.Config import ZShell.Config
import qs.Components import qs.Components
import qs.Services import qs.Services
@@ -10,7 +11,7 @@ CustomText {
Layout.bottomMargin: Tokens.spacing.extraSmall Layout.bottomMargin: Tokens.spacing.extraSmall
Layout.fillWidth: true Layout.fillWidth: true
Layout.leftMargin: Tokens.padding.extraSmall Layout.leftMargin: Tokens.padding.extraSmall
Layout.topMargin: first ? 0 : Tokens.spacing.largeIncreased - ((parent as ColumnLayout).spacing ?? 0) Layout.topMargin: first ? 0 : Tokens.spacing.largeIncreased - ((parent as SnapLayout).spacing ?? 0)
color: Colors.palette.m3onSurfaceVariant color: Colors.palette.m3onSurfaceVariant
elide: Text.ElideRight elide: Text.ElideRight
font: Tokens.font.label.medium font: Tokens.font.label.medium
+27 -3
View File
@@ -41,8 +41,7 @@ orientation: Qt.Vertical
opacity: img.status === Image.Ready ? 0 : 1 opacity: img.status === Image.Ready ? 0 : 1
Behavior on opacity { Behavior on opacity {
Anim { Anim {}
}
} }
sourceComponent: CustomRect { sourceComponent: CustomRect {
color: Colors.palette.m3primaryContainer color: Colors.palette.m3primaryContainer
@@ -66,18 +65,43 @@ orientation: Qt.Vertical
anchors.fill: parent anchors.fill: parent
asynchronous: true asynchronous: true
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
opacity: status === Image.Ready ? 1 : 0
retainWhileLoading: true retainWhileLoading: true
sourceSize: { sourceSize: {
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1; const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
return Qt.size(width * dpr, height * dpr); return Qt.size(width * dpr, height * dpr);
} }
opacity: status === Image.Ready ? 1 : 0
Behavior on opacity { Behavior on opacity {
Anim { Anim {
type: Anim.SlowEffects type: Anim.SlowEffects
} }
} }
Loader {
opacity: root.modelData.isVideo && img.status === Image.Ready ? 1 : 0
active: opacity > 0
anchors.centerIn: parent
Behavior on opacity {
Anim {
type: Anim.SlowEffects
}
}
sourceComponent: CustomRect {
implicitWidth: videoIcon.implicitWidth + Tokens.padding.extraLarge * 2
implicitHeight: implicitWidth
radius: Tokens.rounding.full
color: Colors.tPalette.m3surface
MaterialIcon {
id: videoIcon
anchors.centerIn: parent
text: "video_library"
fontStyle: Tokens.font.icon.extraLarge
}
}
}
} }
} }
} }
+42 -8
View File
@@ -13,9 +13,14 @@ import qs.Services
Item { Item {
id: wrapper id: wrapper
property int buttonRowHeight: Tokens.padding.large * 3 readonly property bool isVideo: Wallpapers.currentIsVideo
readonly property string currentWallpaper: Wallpapers.current
readonly property var currentScreen: screens.length > selectedScreenIndex ? screens[selectedScreenIndex] : null
readonly property real maxZoom: 5.0
readonly property bool changesMade: { readonly property bool changesMade: {
cropRevision; cropRevision;
Wallpapers.cropVersion;
wrapper.currentWallpaper;
const r = cropRectLoader.item; const r = cropRectLoader.item;
if (!r || !currentScreen || scaledImg.status !== Image.Ready) if (!r || !currentScreen || scaledImg.status !== Image.Ready)
@@ -25,9 +30,9 @@ Item {
return Math.abs(r.x - t.x) > 0.5 || Math.abs(r.y - t.y) > 0.5 || Math.abs(r.zoom - t.zoom) > 0.001; return Math.abs(r.x - t.x) > 0.5 || Math.abs(r.y - t.y) > 0.5 || Math.abs(r.zoom - t.zoom) > 0.001;
} }
property int buttonRowHeight: Tokens.padding.large * 3
property int cropRevision: 0 property int cropRevision: 0
readonly property var currentScreen: screens.length > selectedScreenIndex ? screens[selectedScreenIndex] : null
readonly property real maxZoom: 5.0
property var screens: [] property var screens: []
property int selectedScreenIndex: 0 property int selectedScreenIndex: 0
property bool shouldBeActive: true property bool shouldBeActive: true
@@ -113,6 +118,9 @@ Item {
refreshScreens(); refreshScreens();
Qt.callLater(syncCropToScreen); Qt.callLater(syncCropToScreen);
} }
onCurrentWallpaperChanged: {
Qt.callLater(syncCropToScreen);
}
onSelectedScreenIndexChanged: { onSelectedScreenIndexChanged: {
Qt.callLater(syncCropToScreen); Qt.callLater(syncCropToScreen);
} }
@@ -127,7 +135,7 @@ Item {
anchors.rightMargin: Tokens.padding.extraLarge * 2 anchors.rightMargin: Tokens.padding.extraLarge * 2
anchors.top: parent.top anchors.top: parent.top
active: wrapper.screens.length > 1 active: wrapper.screens.length > 1 && !wrapper.isVideo
sourceComponent: ButtonRow { sourceComponent: ButtonRow {
id: screenSelector id: screenSelector
@@ -163,6 +171,7 @@ Item {
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
implicitHeight: 30 implicitHeight: 30
enabled: !wrapper.isVideo
CustomSlider { CustomSlider {
id: zoomSlider id: zoomSlider
@@ -274,7 +283,7 @@ Item {
opacity: status === Image.Ready ? 1 : 0 opacity: status === Image.Ready ? 1 : 0
retainWhileLoading: true retainWhileLoading: true
smooth: true smooth: true
source: Wallpapers.current source: Wallpapers.currentThumbnail
sourceSize.height: { sourceSize.height: {
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1; const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
return imageSlot.height * dpr; return imageSlot.height * dpr;
@@ -285,12 +294,37 @@ Item {
type: Anim.SlowEffects type: Anim.SlowEffects
} }
} }
Loader {
opacity: wrapper.isVideo && scaledImg.status === Image.Ready ? 1 : 0
active: opacity > 0
anchors.centerIn: parent
Behavior on opacity {
Anim {
type: Anim.SlowEffects
}
}
sourceComponent: CustomRect {
implicitWidth: videoIcon.implicitWidth + Tokens.padding.extraLarge * 2
implicitHeight: implicitWidth
radius: Tokens.rounding.full
color: Colors.tPalette.m3surface
MaterialIcon {
id: videoIcon
anchors.centerIn: parent
text: "video_library"
fontStyle: Tokens.font.icon.extraLarge
}
}
}
} }
Loader { Loader {
id: cropRectLoader id: cropRectLoader
active: scaledImg.status === Image.Ready active: scaledImg.status === Image.Ready && !wrapper.isVideo
sourceComponent: Component { sourceComponent: Component {
CustomRect { CustomRect {
@@ -333,7 +367,6 @@ Item {
y = t.y; y = t.y;
} }
// Where the rect should be according to saved data (same rules as a restore).
function savedTarget() { function savedTarget() {
const data = Wallpapers.getCrop(wrapper.currentScreen.name); const data = Wallpapers.getCrop(wrapper.currentScreen.name);
const hasCrop = data && (Math.abs(data.x) > 0.001 || Math.abs(data.y) > 0.001 || Math.abs(data.width - 1.0) > 0.001 || Math.abs(data.height - 1.0) > 0.001); const hasCrop = data && (Math.abs(data.x) > 0.001 || Math.abs(data.y) > 0.001 || Math.abs(data.width - 1.0) > 0.001 || Math.abs(data.height - 1.0) > 0.001);
@@ -436,9 +469,10 @@ Item {
} }
anchors.fill: parent anchors.fill: parent
cursorShape: pressed ? Qt.ClosedHandCursor : insideCropRect(mouseX, mouseY) ? Qt.OpenHandCursor : Qt.PointingHandCursor cursorShape: wrapper.isVideo ? Qt.ArrowCursor : pressed ? Qt.ClosedHandCursor : insideCropRect(mouseX, mouseY) ? Qt.OpenHandCursor : Qt.PointingHandCursor
hoverEnabled: true hoverEnabled: true
preventStealing: true preventStealing: true
enabled: !wrapper.isVideo
onPositionChanged: mouse => { onPositionChanged: mouse => {
if (pressed) { if (pressed) {
+6 -2
View File
@@ -18,10 +18,12 @@ PageBase {
title: qsTr("All apps") title: qsTr("All apps")
SnapLayout { SnapLayout {
orientation: Qt.Vertical
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top anchors.top: parent.top
spacing: Tokens.spacing.extraSmall / 2 spacing: Tokens.spacing.extraSmall / 2
orientation: Qt.Vertical
width: root.cappedWidth width: root.cappedWidth
Repeater { Repeater {
@@ -54,6 +56,7 @@ orientation: Qt.Vertical
anchors.leftMargin: Tokens.padding.largeIncreased anchors.leftMargin: Tokens.padding.largeIncreased
anchors.margins: Tokens.padding.small anchors.margins: Tokens.padding.small
anchors.rightMargin: Tokens.padding.largeIncreased anchors.rightMargin: Tokens.padding.largeIncreased
spacing: Tokens.spacing.medium spacing: Tokens.spacing.medium
IconImage { IconImage {
@@ -63,8 +66,9 @@ orientation: Qt.Vertical
} }
SnapLayout { SnapLayout {
orientation: Qt.Vertical
Layout.fillWidth: true Layout.fillWidth: true
orientation: Qt.Vertical
spacing: 0 spacing: 0
CustomText { CustomText {
@@ -52,11 +52,12 @@ orientation: Qt.Vertical
} }
WallItem { WallItem {
required property FileSystemEntry modelData id: wallItem
required property WallpaperEntry modelData
enabled: modelData enabled: modelData
opacity: modelData ? 1 : 0 opacity: modelData ? 1 : 0
source: String(modelData?.path ?? "") source: String(modelData?.thumbnailPath ?? "")
onClicked: { onClicked: {
Wallpapers.setWallpaper(modelData.path); Wallpapers.setWallpaper(modelData.path);
+48
View File
@@ -13,6 +13,42 @@ import qs.Services
PageBase { PageBase {
id: root id: root
readonly property list<MenuItem> wallTransitions: [
MenuItem {
text: "Bowtie"
},
MenuItem {
text: "Fade"
},
MenuItem {
text: "LinearBlur"
},
MenuItem {
text: "WindowSlices"
},
MenuItem {
text: "WipeRight"
},
MenuItem {
text: "WipeDown"
},
MenuItem {
text: "TilesWave"
},
MenuItem {
text: "SplitSlideInHorizontal"
},
MenuItem {
text: "Hexagonalize"
},
MenuItem {
text: "DoomMelt"
},
MenuItem {
text: "Random"
}
]
title: qsTr("Wallpaper & style") title: qsTr("Wallpaper & style")
SnapLayout { SnapLayout {
@@ -74,6 +110,18 @@ orientation: Qt.Vertical
onToggled: Config.background.enabled = checked onToggled: Config.background.enabled = checked
} }
SelectRow {
Layout.topMargin: Tokens.spacing.extraSmall / 2 - parent.spacing
active: root.wallTransitions[Config.background.wallTransition]
menuItems: root.wallTransitions
menuOnTop: true
settingAnchor: "style-wallpaper-transition"
subtext: qsTr("Currently selected: %1").arg(active.text)
text: qsTr("Wallpaper transition")
onSelected: item => Config.background.wallTransition = root.wallTransitions.indexOf(item)
}
ToggleRow { ToggleRow {
Layout.topMargin: Tokens.spacing.extraSmall / 2 - parent.spacing Layout.topMargin: Tokens.spacing.extraSmall / 2 - parent.spacing
checked: Colors.transparency.enabled checked: Colors.transparency.enabled
@@ -344,7 +344,7 @@ Item {
} }
} }
ClipRect { Item {
id: expandedTool id: expandedTool
anchors.top: collapsedRow.bottom anchors.top: collapsedRow.bottom
@@ -355,8 +355,16 @@ Item {
anchors.topMargin: Tokens.spacing.medium anchors.topMargin: Tokens.spacing.medium
implicitHeight: fullToolCall.implicitHeight + fullToolCall.anchors.margins * 2 implicitHeight: fullToolCall.implicitHeight + fullToolCall.anchors.margins * 2
radius: Tokens.rounding.medium clip: true
color: Colors.palette.m3surfaceContainerLow
CustomRect {
id: bg
anchors.fill: parent
radius: Tokens.rounding.medium
color: Colors.palette.m3surfaceContainerLow
}
CustomText { CustomText {
id: fullToolCall id: fullToolCall
@@ -59,7 +59,6 @@ Item {
id: container id: container
anchors.fill: parent anchors.fill: parent
layer.enabled: opacity < 1
objectName: "ConversationContainer" objectName: "ConversationContainer"
Component.onCompleted: { Component.onCompleted: {
+11 -3
View File
@@ -125,7 +125,7 @@ Item {
current: root.current current: root.current
} }
ClipRect { Item {
id: expandedRect id: expandedRect
anchors.left: parent.left anchors.left: parent.left
@@ -133,11 +133,19 @@ Item {
anchors.top: layout.bottom anchors.top: layout.bottom
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.topMargin: Tokens.spacing.small anchors.topMargin: Tokens.spacing.small
color: Colors.palette.m3surfaceContainerLow
implicitHeight: expandedContent.contentHeight + expandedContent.anchors.margins * 2 implicitHeight: expandedContent.contentHeight + expandedContent.anchors.margins * 2
opacity: root.expanded ? 1 : 0 opacity: root.expanded ? 1 : 0
radius: Tokens.rounding.medium
visible: opacity > 0 visible: opacity > 0
clip: true
CustomRect {
id: bg
anchors.fill: parent
radius: Tokens.rounding.medium
color: Colors.palette.m3surfaceContainerLow
}
Behavior on opacity { Behavior on opacity {
Anim {} Anim {}
+3 -1
View File
@@ -4,6 +4,8 @@ import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell import Quickshell
import ZShell.Config import ZShell.Config
import ZShell.Components
import ZShell.Services
import qs.Components import qs.Components
import qs.Services import qs.Services
import qs.Daemons import qs.Daemons
@@ -14,7 +16,7 @@ CustomRect {
readonly property CustomText body: (expandedContent.item as ExpandedBody)?.body ?? null readonly property CustomText body: (expandedContent.item as ExpandedBody)?.body ?? null
required property bool expanded required property bool expanded
required property NotifServer.Notif modelData required property NotificationRow modelData
readonly property real nonAnimHeight: expanded ? summary.implicitHeight + expandedContent.implicitHeight + expandedContent.anchors.topMargin + Tokens.padding.medium * 2 : summaryHeightMetrics.height readonly property real nonAnimHeight: expanded ? summary.implicitHeight + expandedContent.implicitHeight + expandedContent.anchors.topMargin + Tokens.padding.medium * 2 : summaryHeightMetrics.height
required property ScreenState screenState required property ScreenState screenState
+2 -3
View File
@@ -9,12 +9,11 @@ import ZShell.Components
import qs.Components import qs.Components
import qs.Effects import qs.Effects
import qs.Services import qs.Services
import qs.Daemons
Item { Item {
id: root id: root
required property NotifServer.Notif notif required property NotificationRow notif
Layout.fillWidth: true Layout.fillWidth: true
implicitHeight: actionList.implicitHeight implicitHeight: actionList.implicitHeight
@@ -30,7 +29,7 @@ Item {
{ {
isClose: true isClose: true
}, },
...(root.notif?.actions ?? []), ...Array.from(root.notif?.actions ?? []),
{ {
isCopy: true isCopy: true
} }
+3 -3
View File
@@ -81,9 +81,8 @@ Item {
Behavior on opacity { Behavior on opacity {
Anim {} Anim {}
} }
sourceComponent: SnapLayout { sourceComponent: ColumnLayout {
spacing: 20 spacing: 20
orientation: Qt.Vertical
CustomText { CustomText {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
@@ -94,13 +93,14 @@ Item {
} }
} }
CustomFlickable { KineticFlickable {
id: view id: view
anchors.fill: parent anchors.fill: parent
contentHeight: notifList.implicitHeight contentHeight: notifList.implicitHeight
contentWidth: width contentWidth: width
flickableDirection: Flickable.VerticalFlick flickableDirection: Flickable.VerticalFlick
clip: true
CustomScrollBar.vertical: CustomScrollBar { CustomScrollBar.vertical: CustomScrollBar {
flickable: view flickable: view
+2 -2
View File
@@ -3,8 +3,8 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import Quickshell import Quickshell
import ZShell.Components import ZShell.Components
import ZShell.Services
import ZShell.Config import ZShell.Config
import qs.Daemons
import qs.Components import qs.Components
import qs.Services import qs.Services
@@ -35,7 +35,7 @@ LazyListView {
id: notif id: notif
required property int index required property int index
required property NotifServer.Notif modelData required property NotificationRow modelData
property int startY property int startY
LazyListView.preferredHeight: modelData?.closed || LazyListView.removing ? 0 : notifInner.nonAnimHeight LazyListView.preferredHeight: modelData?.closed || LazyListView.removing ? 0 : notifInner.nonAnimHeight
+9
View File
@@ -6,6 +6,7 @@ find_package(Qt6 REQUIRED COMPONENTS
GuiPrivate GuiPrivate
Quick Quick
CanvasPainter CanvasPainter
CanvasPainterTools
QuickPrivate QuickPrivate
QuickLayouts QuickLayouts
QuickLayoutsPrivate QuickLayoutsPrivate
@@ -23,6 +24,14 @@ pkg_check_modules(WAYLAND_CLIENT REQUIRED IMPORTED_TARGET wayland-client)
pkg_check_modules(EGL REQUIRED IMPORTED_TARGET egl) pkg_check_modules(EGL REQUIRED IMPORTED_TARGET egl)
pkg_check_modules(WAYLAND_EGL REQUIRED IMPORTED_TARGET wayland-egl) pkg_check_modules(WAYLAND_EGL REQUIRED IMPORTED_TARGET wayland-egl)
pkg_check_modules(GL REQUIRED IMPORTED_TARGET gl) pkg_check_modules(GL REQUIRED IMPORTED_TARGET gl)
pkg_check_modules(MPV REQUIRED IMPORTED_TARGET mpv)
pkg_check_modules(AVCODEC REQUIRED IMPORTED_TARGET libavcodec)
pkg_check_modules(AVFORMAT REQUIRED IMPORTED_TARGET libavformat)
pkg_check_modules(AVUTIL REQUIRED IMPORTED_TARGET libavutil)
pkg_check_modules(LIBVA QUIET IMPORTED_TARGET libva)
find_package(CUDAToolkit QUIET)
pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED) pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED)
pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED) pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED)
pkg_check_modules(Aubio IMPORTED_TARGET aubio REQUIRED) pkg_check_modules(Aubio IMPORTED_TARGET aubio REQUIRED)
+39 -2
View File
@@ -2,7 +2,17 @@
#include "blobinvertedrect.hpp" #include "blobinvertedrect.hpp"
#include "blobshape.hpp" #include "blobshape.hpp"
BlobGroup::BlobGroup(QObject* parent) : QObject(parent) {} #include <algorithm>
namespace {
constexpr int k_pumpMs = 16;
} // namespace
BlobGroup::BlobGroup(QObject* parent) : QObject(parent) {
m_physicsPump.setInterval(k_pumpMs);
m_physicsPump.setTimerType(Qt::CoarseTimer);
connect(&m_physicsPump, &QTimer::timeout, this, &BlobGroup::pumpPhysics);
}
BlobGroup::~BlobGroup() { BlobGroup::~BlobGroup() {
for (auto* shape : std::as_const(m_shapes)) for (auto* shape : std::as_const(m_shapes))
@@ -12,6 +22,7 @@ BlobGroup::~BlobGroup() {
} }
void BlobGroup::setSmoothing(qreal s) { void BlobGroup::setSmoothing(qreal s) {
if (s < 0.5) s = 0.5;
if (qFuzzyCompare(m_smoothing, s)) return; if (qFuzzyCompare(m_smoothing, s)) return;
m_smoothing = s; m_smoothing = s;
emit smoothingChanged(); emit smoothingChanged();
@@ -73,7 +84,6 @@ void BlobGroup::markShapeDirty(BlobShape* source) {
source->polish(); source->polish();
source->update(); source->update();
// Use cached padded rects to find spatial neighbors
const float pad = static_cast<float>(m_smoothing) * 2.0f; const float pad = static_cast<float>(m_smoothing) * 2.0f;
const QRectF srcRect( const QRectF srcRect(
static_cast<double>(source->m_cachedPaddedX - pad), static_cast<double>(source->m_cachedPaddedX - pad),
@@ -103,6 +113,33 @@ void BlobGroup::markShapeDirty(BlobShape* source) {
void BlobGroup::ensurePhysicsUpdated() { void BlobGroup::ensurePhysicsUpdated() {
if (m_physicsUpdated) return; if (m_physicsUpdated) return;
m_physicsUpdated = true; m_physicsUpdated = true;
for (auto* shape : std::as_const(m_shapes)) {
shape->updatePhysics();
if (shape->needsFrame()) {
shape->polish();
shape->update();
}
}
}
void BlobGroup::requestPhysics() {
if (!m_physicsPump.isActive()) m_physicsPump.start();
}
void BlobGroup::pumpPhysics() {
for (auto* shape : std::as_const(m_shapes)) for (auto* shape : std::as_const(m_shapes))
shape->updatePhysics(); shape->updatePhysics();
const bool anyMoving = std::any_of(
m_shapes.cbegin(), m_shapes.cend(), [](const BlobShape* shape) {
return shape->needsFrame();
});
if (!anyMoving) {
m_physicsPump.stop();
return;
}
markDirty();
m_physicsUpdated = true;
} }
+6
View File
@@ -4,6 +4,7 @@
#include <qlist.h> #include <qlist.h>
#include <qobject.h> #include <qobject.h>
#include <qqmlengine.h> #include <qqmlengine.h>
#include <qtimer.h>
class BlobShape; class BlobShape;
class BlobInvertedRect; class BlobInvertedRect;
@@ -51,16 +52,21 @@ class BlobGroup : public QObject {
void markShapeDirty(BlobShape* source); void markShapeDirty(BlobShape* source);
void ensurePhysicsUpdated(); void ensurePhysicsUpdated();
void requestPhysics();
signals: signals:
void smoothingChanged(); void smoothingChanged();
void colorChanged(); void colorChanged();
void cornerFillChanged(); void cornerFillChanged();
private: private:
void pumpPhysics();
qreal m_smoothing = 32.0; qreal m_smoothing = 32.0;
QColor m_color{0x44, 0x88, 0xff}; QColor m_color{0x44, 0x88, 0xff};
bool m_cornerFill = true; bool m_cornerFill = true;
QList<BlobShape*> m_shapes; QList<BlobShape*> m_shapes;
BlobInvertedRect* m_invertedRect = nullptr; BlobInvertedRect* m_invertedRect = nullptr;
bool m_physicsUpdated = false; bool m_physicsUpdated = false;
QTimer m_physicsPump;
}; };
+65 -39
View File
@@ -29,8 +29,15 @@ static float cornerFillFactor(float sd, float smoothFactor) {
return std::max(outside, inside); return std::max(outside, inside);
} }
namespace {
constexpr int k_pumpMs = 16;
} // namespace
BlobGroup2::BlobGroup2(QQuickItem* parent) : QQuickItem(parent) { BlobGroup2::BlobGroup2(QQuickItem* parent) : QQuickItem(parent) {
setFlag(ItemHasContents); setFlag(ItemHasContents);
m_physicsPump.setInterval(k_pumpMs);
m_physicsPump.setTimerType(Qt::CoarseTimer);
connect(&m_physicsPump, &QTimer::timeout, this, &BlobGroup2::pumpPhysics);
} }
BlobGroup2::~BlobGroup2() { BlobGroup2::~BlobGroup2() {
@@ -41,6 +48,7 @@ BlobGroup2::~BlobGroup2() {
} }
void BlobGroup2::setSmoothing(qreal s) { void BlobGroup2::setSmoothing(qreal s) {
if (s < 0.5) s = 0.5;
if (qFuzzyCompare(m_smoothing, s)) return; if (qFuzzyCompare(m_smoothing, s)) return;
m_smoothing = s; m_smoothing = s;
emit smoothingChanged(); emit smoothingChanged();
@@ -101,17 +109,11 @@ void BlobGroup2::clearInvertedRect(BlobInvertedRect2* rect) {
void BlobGroup2::markDirty() { void BlobGroup2::markDirty() {
m_physicsUpdated = false; m_physicsUpdated = false;
m_dataBuilt = false; m_dataBuilt = false;
// Physics runs during polish, before the render pass. The panel content is
// transformed by deformMatrix from QML, so the matrix has to be final for this
// frame; building the data in updatePaintNode would let the transform lag one
// frame behind the material.
polish(); polish();
update(); update();
} }
void BlobGroup2::updatePolish() { void BlobGroup2::updatePolish() {
// The group is the only thing that draws, so it also drives the shapes'
// physics.
ensureDataBuilt(); ensureDataBuilt();
} }
@@ -122,32 +124,53 @@ void BlobGroup2::ensurePhysicsUpdated() {
shape->updatePhysics(); shape->updatePhysics();
} }
void BlobGroup2::requestPhysics() {
if (!m_physicsPump.isActive()) m_physicsPump.start();
}
void BlobGroup2::pumpPhysics() {
for (auto* shape : std::as_const(m_shapes))
shape->updatePhysics();
m_physicsUpdated = true;
const bool anyMoving = std::any_of(
m_shapes.cbegin(), m_shapes.cend(), [](const BlobShape2* shape) {
return shape->needsFrame();
});
if (!anyMoving) {
m_physicsPump.stop();
return;
}
m_dataBuilt = false;
polish();
update();
}
void BlobGroup2::ensureDataBuilt() { void BlobGroup2::ensureDataBuilt() {
if (m_dataBuilt) return; if (m_dataBuilt) return;
ensurePhysicsUpdated(); ensurePhysicsUpdated();
// Physics can request the next frame through markDirty(); the data is
// refreshed here anyway, so the flag is set after it runs.
m_dataBuilt = true; m_dataBuilt = true;
m_data.rects.clear(); m_data.rects.clear();
m_bounds = QRectF(); m_bounds = QRectF();
m_data.hasInverted = false;
m_data.invertedRadius = 0;
memset(m_data.invertedOuter, 0, sizeof(m_data.invertedOuter));
memset(m_data.invertedInner, 0, sizeof(m_data.invertedInner));
memset(m_data.invertedOuterRadii, 0, sizeof(m_data.invertedOuterRadii));
const float smoothFactor = static_cast<float>(m_smoothing); const float smoothFactor = static_cast<float>(m_smoothing);
// The same reach the shader uses to decide whether a rect can contribute, plus
// the shadow, so every pixel the shader can give a value to is inside the node.
const double extent = static_cast<double>(smoothFactor) * 1.5 + const double extent = static_cast<double>(smoothFactor) * 1.5 +
static_cast<double>(m_shadowRadius); static_cast<double>(m_shadowRadius);
// Group order, no pruning by proximity: the shader sees this list for every pixel. A const bool dropAtRest = m_invertedRect != nullptr;
// shape that is not pushing is not in the list at all: shapes here are data for the
// group draw, so leaving a panel at rest out means it cannot define any part of the
// edge. A closing panel is still in the list while its curve runs out, even with no
// size left, which is what keeps the bounce.
// entryShapes maps a shared entry back to the shape it came from.
QVector<BlobShape2*> entryShapes; QVector<BlobShape2*> entryShapes;
for (auto* shape : std::as_const(m_shapes)) { for (auto* shape : std::as_const(m_shapes)) {
if (!shape->isMoving()) continue; if (dropAtRest && !shape->isMoving()) continue;
if (shape->width() <= 0 || shape->height() <= 0) continue; if (shape->width() <= 0 || shape->height() <= 0) continue;
if (static_cast<int>(m_data.rects.size()) >= BlobGroupData::kMaxRects) if (static_cast<int>(m_data.rects.size()) >= BlobGroupData::kMaxRects)
continue; continue;
@@ -194,7 +217,6 @@ void BlobGroup2::ensureDataBuilt() {
const int count = static_cast<int>(m_data.rects.size()); const int count = static_cast<int>(m_data.rects.size());
// Exclusion gates only the smooth blend.
for (int i = 0; i < count; ++i) { for (int i = 0; i < count; ++i) {
int mask = 0; int mask = 0;
BlobShape2* const si = entryShapes[i]; BlobShape2* const si = entryShapes[i];
@@ -214,10 +236,11 @@ void BlobGroup2::ensureDataBuilt() {
BlobShape2* const si = entryShapes[i]; BlobShape2* const si = entryShapes[i];
float fTr = 1.0f, fBr = 1.0f, fBl = 1.0f, fTl = 1.0f; float fTr = 1.0f, fBr = 1.0f, fBl = 1.0f, fTl = 1.0f;
const float cTrX = ri.cx + ri.hw, cTrY = ri.cy - ri.hh; const float dcx = ri.cx + ri.offsetX, dcy = ri.cy + ri.offsetY;
const float cBrX = ri.cx + ri.hw, cBrY = ri.cy + ri.hh; const float cTrX = dcx + ri.hw, cTrY = dcy - ri.hh;
const float cBlX = ri.cx - ri.hw, cBlY = ri.cy + ri.hh; const float cBrX = dcx + ri.hw, cBrY = dcy + ri.hh;
const float cTlX = ri.cx - ri.hw, cTlY = ri.cy - ri.hh; const float cBlX = dcx - ri.hw, cBlY = dcy + ri.hh;
const float cTlX = dcx - ri.hw, cTlY = dcy - ri.hh;
if (cornerFill) { if (cornerFill) {
for (int j = 0; j < count; ++j) { for (int j = 0; j < count; ++j) {
@@ -227,37 +250,41 @@ void BlobGroup2::ensureDataBuilt() {
if (si->isCornerExcluded(sj) || sj->isCornerExcluded(si)) if (si->isCornerExcluded(sj) || sj->isCornerExcluded(si))
continue; continue;
const auto& rj = m_data.rects[j]; const auto& rj = m_data.rects[j];
const float jcx = rj.cx + rj.offsetX;
const float jcy = rj.cy + rj.offsetY;
fTr = std::min( fTr = std::min(
fTr, fTr,
cornerFillFactor( cornerFillFactor(
cpuSdBox(cTrX, cTrY, rj.cx, rj.cy, rj.hw, rj.hh), cpuSdBox(cTrX, cTrY, jcx, jcy, rj.hw, rj.hh),
smoothFactor)); smoothFactor));
fBr = std::min( fBr = std::min(
fBr, fBr,
cornerFillFactor( cornerFillFactor(
cpuSdBox(cBrX, cBrY, rj.cx, rj.cy, rj.hw, rj.hh), cpuSdBox(cBrX, cBrY, jcx, jcy, rj.hw, rj.hh),
smoothFactor)); smoothFactor));
fBl = std::min( fBl = std::min(
fBl, fBl,
cornerFillFactor( cornerFillFactor(
cpuSdBox(cBlX, cBlY, rj.cx, rj.cy, rj.hw, rj.hh), cpuSdBox(cBlX, cBlY, jcx, jcy, rj.hw, rj.hh),
smoothFactor)); smoothFactor));
fTl = std::min( fTl = std::min(
fTl, fTl,
cornerFillFactor( cornerFillFactor(
cpuSdBox(cTlX, cTlY, rj.cx, rj.cy, rj.hw, rj.hh), cpuSdBox(cTlX, cTlY, jcx, jcy, rj.hw, rj.hh),
smoothFactor)); smoothFactor));
} }
} }
ri.radius[0] = std::max(ri.radius[0] * fTr, minR); ri.radius[0] = ri.radius[0] > 0.0f ? std::max(ri.radius[0] * fTr, minR)
ri.radius[1] = std::max(ri.radius[1] * fBr, minR); : 0.0f;
ri.radius[2] = std::max(ri.radius[2] * fBl, minR); ri.radius[1] = ri.radius[1] > 0.0f ? std::max(ri.radius[1] * fBr, minR)
ri.radius[3] = std::max(ri.radius[3] * fTl, minR); : 0.0f;
ri.radius[2] = ri.radius[2] > 0.0f ? std::max(ri.radius[2] * fBl, minR)
: 0.0f;
ri.radius[3] = ri.radius[3] > 0.0f ? std::max(ri.radius[3] * fTl, minR)
: 0.0f;
} }
// Inverted rect. The frame term is evaluated for every pixel in the bounds,
// so the group flag cannot be gated per shape.
if (!m_invertedRect) return; if (!m_invertedRect) return;
auto* inv = static_cast<BlobShape2*>(m_invertedRect); auto* inv = static_cast<BlobShape2*>(m_invertedRect);
@@ -297,6 +324,8 @@ void BlobGroup2::ensureDataBuilt() {
m_data.invertedInner[2] = innerHW; m_data.invertedInner[2] = innerHW;
m_data.invertedInner[3] = innerHH; m_data.invertedInner[3] = innerHH;
inv->outerCornerRadii(m_data.invertedOuterRadii);
m_bounds = m_bounds.united(QRectF( m_bounds = m_bounds.united(QRectF(
static_cast<double>(outerCX) - static_cast<double>(outerHW) - extent, static_cast<double>(outerCX) - static_cast<double>(outerHW) - extent,
static_cast<double>(outerCY) - static_cast<double>(outerHH) - extent, static_cast<double>(outerCY) - static_cast<double>(outerHH) - extent,
@@ -311,7 +340,6 @@ void BlobGroup2::fillMaterial(BlobMaterial* material) {
material->m_paddedH = static_cast<float>(m_bounds.height()); material->m_paddedH = static_cast<float>(m_bounds.height());
material->m_smoothFactor = static_cast<float>(m_smoothing); material->m_smoothFactor = static_cast<float>(m_smoothing);
// The single group draw has no owner, so the shader skips the ownership test.
material->m_myIndex = -2; material->m_myIndex = -2;
material->m_color = m_color; material->m_color = m_color;
material->m_hasInverted = m_data.hasInverted ? 1 : 0; material->m_hasInverted = m_data.hasInverted ? 1 : 0;
@@ -324,6 +352,10 @@ void BlobGroup2::fillMaterial(BlobMaterial* material) {
material->m_invertedInner, material->m_invertedInner,
data().invertedInner, data().invertedInner,
sizeof(data().invertedInner)); sizeof(data().invertedInner));
memcpy(
material->m_invertedOuterRadii,
data().invertedOuterRadii,
sizeof(data().invertedOuterRadii));
material->m_shadowRadius = static_cast<float>(m_shadowRadius); material->m_shadowRadius = static_cast<float>(m_shadowRadius);
material->m_shadowColor[0] = static_cast<float>(m_shadowColor.redF()); material->m_shadowColor[0] = static_cast<float>(m_shadowColor.redF());
@@ -339,8 +371,6 @@ void BlobGroup2::fillMaterial(BlobMaterial* material) {
} }
QSGNode* BlobGroup2::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) { QSGNode* BlobGroup2::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) {
// Data is built during polish. This only covers a render pass that happens
// without one.
ensureDataBuilt(); ensureDataBuilt();
if (m_data.rects.isEmpty() && !m_data.hasInverted) { if (m_data.rects.isEmpty() && !m_data.hasInverted) {
@@ -364,10 +394,6 @@ QSGNode* BlobGroup2::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) {
node->setFlag(QSGNode::OwnsMaterial); node->setFlag(QSGNode::OwnsMaterial);
} }
// The shader evaluates the field at the scene position a vertex maps to, so
// the group item itself must not be transformed. The vertex positions are the
// bounds in this item's frame; the material carries the same rect in scene
// coordinates.
auto* v = node->geometry()->vertexDataAsTexturedPoint2D(); auto* v = node->geometry()->vertexDataAsTexturedPoint2D();
const QPointF p0 = mapFromScene(m_bounds.topLeft()); const QPointF p0 = mapFromScene(m_bounds.topLeft());
const QPointF p1 = mapFromScene(m_bounds.bottomRight()); const QPointF p1 = mapFromScene(m_bounds.bottomRight());
+6 -14
View File
@@ -9,22 +9,13 @@
#include <qquickitem.h> #include <qquickitem.h>
#include <qrect.h> #include <qrect.h>
#include <qsgnode.h> #include <qsgnode.h>
#include <qtimer.h>
#include <qvector.h> #include <qvector.h>
class BlobShape2; class BlobShape2;
class BlobInvertedRect2; class BlobInvertedRect2;
// One shared valuation for a whole group, drawn by one node.
//
// The shapes here are data only: they say where the material should be. The group
// draws a single quad over everything the shader can evaluate, so a pixel cannot
// be evaluated by one shape and missed by another, and no second draw can
// disagree with the first. That is what makes overlapping shapes read as one
// surface - one value, one writer, one alpha per pixel.
struct BlobGroupData { struct BlobGroupData {
// The shader reads rectData[80] = 16 entries, and excludeMask is a 32-bit
// mask. The shared list is capped at 16, in group order, so the cap is the
// same list for every pixel.
static constexpr int kMaxRects = 16; static constexpr int kMaxRects = 16;
QVector<BlobRectData> rects; QVector<BlobRectData> rects;
@@ -32,6 +23,7 @@ struct BlobGroupData {
float invertedRadius = 0; float invertedRadius = 0;
float invertedOuter[4] = {}; float invertedOuter[4] = {};
float invertedInner[4] = {}; float invertedInner[4] = {};
float invertedOuterRadii[4] = {};
}; };
class BlobGroup2 : public QQuickItem { class BlobGroup2 : public QQuickItem {
@@ -91,10 +83,10 @@ class BlobGroup2 : public QQuickItem {
[[nodiscard]] const BlobGroupData& data() const { return m_data; } [[nodiscard]] const BlobGroupData& data() const { return m_data; }
// One draw per group, so a change to any shape changes the value for every
// pixel. Refresh the whole group.
void markDirty(); void markDirty();
void requestPhysics();
signals: signals:
void smoothingChanged(); void smoothingChanged();
void colorChanged(); void colorChanged();
@@ -110,6 +102,7 @@ class BlobGroup2 : public QQuickItem {
void ensurePhysicsUpdated(); void ensurePhysicsUpdated();
void ensureDataBuilt(); void ensureDataBuilt();
void fillMaterial(BlobMaterial* material); void fillMaterial(BlobMaterial* material);
void pumpPhysics();
qreal m_smoothing = 32.0; qreal m_smoothing = 32.0;
QColor m_color{0x44, 0x88, 0xff}; QColor m_color{0x44, 0x88, 0xff};
@@ -120,9 +113,8 @@ class BlobGroup2 : public QQuickItem {
BlobInvertedRect2* m_invertedRect = nullptr; BlobInvertedRect2* m_invertedRect = nullptr;
bool m_physicsUpdated = false; bool m_physicsUpdated = false;
bool m_dataBuilt = false; bool m_dataBuilt = false;
QTimer m_physicsPump;
BlobGroupData m_data; BlobGroupData m_data;
// Scene-space union of what the shader can evaluate for any shape. The node
// covers exactly this, so gaps are not possible.
QRectF m_bounds; QRectF m_bounds;
}; };
+8 -14
View File
@@ -11,28 +11,24 @@
BlobInvertedRect::BlobInvertedRect(QQuickItem* parent) : BlobShape(parent) {} BlobInvertedRect::BlobInvertedRect(QQuickItem* parent) : BlobShape(parent) {}
static void setFrameIndices(quint16* idx) { static void setFrameIndices(quint16* idx) {
// Top strip: 0-1-4, 1-5-4
idx[0] = 0; idx[0] = 0;
idx[1] = 1; idx[1] = 1;
idx[2] = 4; idx[2] = 4;
idx[3] = 1; idx[3] = 1;
idx[4] = 5; idx[4] = 5;
idx[5] = 4; idx[5] = 4;
// Right strip: 1-2-5, 2-6-5
idx[6] = 1; idx[6] = 1;
idx[7] = 2; idx[7] = 2;
idx[8] = 5; idx[8] = 5;
idx[9] = 2; idx[9] = 2;
idx[10] = 6; idx[10] = 6;
idx[11] = 5; idx[11] = 5;
// Bottom strip: 2-3-6, 3-7-6
idx[12] = 2; idx[12] = 2;
idx[13] = 3; idx[13] = 3;
idx[14] = 6; idx[14] = 6;
idx[15] = 3; idx[15] = 3;
idx[16] = 7; idx[16] = 7;
idx[17] = 6; idx[17] = 6;
// Left strip: 3-0-7, 0-4-7
idx[18] = 3; idx[18] = 3;
idx[19] = 0; idx[19] = 0;
idx[20] = 7; idx[20] = 7;
@@ -50,17 +46,20 @@ QSGNode* BlobInvertedRect::updatePaintNode(
const float pad = static_cast<float>(m_group->smoothing()); const float pad = static_cast<float>(m_group->smoothing());
// Compute inner hole boundary in local coords
// Inset past the inner border edge by 2x smoothing to cover the blend zone
const float inset = pad * 2.0f; const float inset = pad * 2.0f;
const float holeLeft = static_cast<float>(m_borderLeft) + inset; const float holeLeft = static_cast<float>(m_borderLeft) + inset;
const float holeTop = static_cast<float>(m_borderTop) + inset; const float holeTop = static_cast<float>(m_borderTop) + inset;
const float holeRight = static_cast<float>(width() - m_borderRight) - inset; const float holeRight = static_cast<float>(width() - m_borderRight) - inset;
const float holeBot = static_cast<float>(height() - m_borderBottom) - inset; const float holeBot = static_cast<float>(height() - m_borderBottom) - inset;
// If the hole is too small or invalid, fall back to full quad if (holeLeft >= holeRight || holeTop >= holeBot) {
if (holeLeft >= holeRight || holeTop >= holeBot) auto* oldGeoNode = static_cast<QSGGeometryNode*>(oldNode);
if (oldNode && oldGeoNode->geometry()->vertexCount() != 4) {
delete oldNode;
oldNode = nullptr;
}
return BlobShape::updatePaintNode(oldNode, nullptr); return BlobShape::updatePaintNode(oldNode, nullptr);
}
auto* node = static_cast<QSGGeometryNode*>(oldNode); auto* node = static_cast<QSGGeometryNode*>(oldNode);
@@ -87,7 +86,6 @@ QSGNode* BlobInvertedRect::updatePaintNode(
node->setFlag(QSGNode::OwnsMaterial); node->setFlag(QSGNode::OwnsMaterial);
} }
// Outer bounds (local coords)
const float x0 = static_cast<float>(m_localPaddedRect.x()); const float x0 = static_cast<float>(m_localPaddedRect.x());
const float y0 = static_cast<float>(m_localPaddedRect.y()); const float y0 = static_cast<float>(m_localPaddedRect.y());
const float x1 = x0 + static_cast<float>(m_localPaddedRect.width()); const float x1 = x0 + static_cast<float>(m_localPaddedRect.width());
@@ -95,15 +93,12 @@ QSGNode* BlobInvertedRect::updatePaintNode(
const float w = x1 - x0; const float w = x1 - x0;
const float h = y1 - y0; const float h = y1 - y0;
// Update vertex positions and texture coordinates
auto* v = node->geometry()->vertexDataAsTexturedPoint2D(); auto* v = node->geometry()->vertexDataAsTexturedPoint2D();
// Outer corners
v[0].set(x0, y0, 0.0f, 0.0f); v[0].set(x0, y0, 0.0f, 0.0f);
v[1].set(x1, y0, 1.0f, 0.0f); v[1].set(x1, y0, 1.0f, 0.0f);
v[2].set(x1, y1, 1.0f, 1.0f); v[2].set(x1, y1, 1.0f, 1.0f);
v[3].set(x0, y1, 0.0f, 1.0f); v[3].set(x0, y1, 0.0f, 1.0f);
// Inner corners (hole)
v[4].set(holeLeft, holeTop, (holeLeft - x0) / w, (holeTop - y0) / h); v[4].set(holeLeft, holeTop, (holeLeft - x0) / w, (holeTop - y0) / h);
v[5].set(holeRight, holeTop, (holeRight - x0) / w, (holeTop - y0) / h); v[5].set(holeRight, holeTop, (holeRight - x0) / w, (holeTop - y0) / h);
v[6].set(holeRight, holeBot, (holeRight - x0) / w, (holeBot - y0) / h); v[6].set(holeRight, holeBot, (holeRight - x0) / w, (holeBot - y0) / h);
@@ -111,7 +106,6 @@ QSGNode* BlobInvertedRect::updatePaintNode(
node->markDirty(QSGNode::DirtyGeometry); node->markDirty(QSGNode::DirtyGeometry);
// Update material uniforms
auto* material = static_cast<BlobMaterial*>(node->material()); auto* material = static_cast<BlobMaterial*>(node->material());
material->m_paddedX = m_cachedPaddedX; material->m_paddedX = m_cachedPaddedX;
material->m_paddedY = m_cachedPaddedY; material->m_paddedY = m_cachedPaddedY;
@@ -218,7 +212,7 @@ void BlobInvertedRect::outerCornerRadii(float out[4]) const {
const auto base = std::min(static_cast<float>(m_outerRadius), maxR); const auto base = std::min(static_cast<float>(m_outerRadius), maxR);
out[0] = std::min( out[0] = std::min(
m_outerTopRightRadius >= 0 ? static_cast<float>(m_outerTopRightRadius) m_outerTopRightRadius >= 0 ? static_cast<float>(m_outerTopRightRadius)
: base, : base,
maxR); maxR);
out[1] = std::min( out[1] = std::min(
m_outerBottomRightRadius >= 0 m_outerBottomRightRadius >= 0
+6 -3
View File
@@ -59,12 +59,15 @@ class BlobInvertedRect : public BlobShape {
void setOuterRadius(qreal v); void setOuterRadius(qreal v);
// Per-corner outer radii. -1 falls back to outerRadius (mirrors BlobRect). [[nodiscard]] qreal outerTopLeftRadius() const {
[[nodiscard]] qreal outerTopLeftRadius() const { return m_outerTopLeftRadius; } return m_outerTopLeftRadius;
}
void setOuterTopLeftRadius(qreal v); void setOuterTopLeftRadius(qreal v);
[[nodiscard]] qreal outerTopRightRadius() const { return m_outerTopRightRadius; } [[nodiscard]] qreal outerTopRightRadius() const {
return m_outerTopRightRadius;
}
void setOuterTopRightRadius(qreal v); void setOuterTopRightRadius(qreal v);
@@ -1,6 +1,8 @@
#include "blobinvertedrect2.hpp" #include "blobinvertedrect2.hpp"
#include "blobgroup2.hpp" #include "blobgroup2.hpp"
#include <algorithm>
BlobInvertedRect2::BlobInvertedRect2(QQuickItem* parent) : BlobShape2(parent) {} BlobInvertedRect2::BlobInvertedRect2(QQuickItem* parent) : BlobShape2(parent) {}
BlobInvertedRect2::~BlobInvertedRect2() { BlobInvertedRect2::~BlobInvertedRect2() {
@@ -35,6 +37,64 @@ void BlobInvertedRect2::setBorderBottom(qreal v) {
if (m_group) m_group->markDirty(); if (m_group) m_group->markDirty();
} }
void BlobInvertedRect2::setOuterRadius(qreal v) {
if (qFuzzyCompare(m_outerRadius, v)) return;
m_outerRadius = v;
emit outerRadiusChanged();
if (m_group) m_group->markDirty();
}
void BlobInvertedRect2::setOuterTopLeftRadius(qreal v) {
if (qFuzzyCompare(m_outerTopLeftRadius, v)) return;
m_outerTopLeftRadius = v;
emit outerTopLeftRadiusChanged();
if (m_group) m_group->markDirty();
}
void BlobInvertedRect2::setOuterTopRightRadius(qreal v) {
if (qFuzzyCompare(m_outerTopRightRadius, v)) return;
m_outerTopRightRadius = v;
emit outerTopRightRadiusChanged();
if (m_group) m_group->markDirty();
}
void BlobInvertedRect2::setOuterBottomLeftRadius(qreal v) {
if (qFuzzyCompare(m_outerBottomLeftRadius, v)) return;
m_outerBottomLeftRadius = v;
emit outerBottomLeftRadiusChanged();
if (m_group) m_group->markDirty();
}
void BlobInvertedRect2::setOuterBottomRightRadius(qreal v) {
if (qFuzzyCompare(m_outerBottomRightRadius, v)) return;
m_outerBottomRightRadius = v;
emit outerBottomRightRadiusChanged();
if (m_group) m_group->markDirty();
}
void BlobInvertedRect2::outerCornerRadii(float out[4]) const {
const auto maxR = static_cast<float>(std::min(width(), height())) * 0.5f;
const auto base = std::min(static_cast<float>(m_outerRadius), maxR);
out[0] = std::min(
m_outerTopRightRadius >= 0 ? static_cast<float>(m_outerTopRightRadius)
: base,
maxR);
out[1] = std::min(
m_outerBottomRightRadius >= 0
? static_cast<float>(m_outerBottomRightRadius)
: base,
maxR);
out[2] = std::min(
m_outerBottomLeftRadius >= 0
? static_cast<float>(m_outerBottomLeftRadius)
: base,
maxR);
out[3] = std::min(
m_outerTopLeftRadius >= 0 ? static_cast<float>(m_outerTopLeftRadius)
: base,
maxR);
}
void BlobInvertedRect2::registerWithGroup() { void BlobInvertedRect2::registerWithGroup() {
if (m_group) m_group->setInvertedRect(this); if (m_group) m_group->setInvertedRect(this);
} }
@@ -19,6 +19,21 @@ class BlobInvertedRect2 : public BlobShape2 {
Q_PROPERTY( Q_PROPERTY(
qreal borderBottom READ borderBottom WRITE setBorderBottom NOTIFY qreal borderBottom READ borderBottom WRITE setBorderBottom NOTIFY
borderBottomChanged) borderBottomChanged)
Q_PROPERTY(
qreal outerRadius READ outerRadius WRITE setOuterRadius NOTIFY
outerRadiusChanged)
Q_PROPERTY(
qreal outerTopLeftRadius READ outerTopLeftRadius WRITE
setOuterTopLeftRadius NOTIFY outerTopLeftRadiusChanged)
Q_PROPERTY(
qreal outerTopRightRadius READ outerTopRightRadius WRITE
setOuterTopRightRadius NOTIFY outerTopRightRadiusChanged)
Q_PROPERTY(
qreal outerBottomLeftRadius READ outerBottomLeftRadius WRITE
setOuterBottomLeftRadius NOTIFY outerBottomLeftRadiusChanged)
Q_PROPERTY(
qreal outerBottomRightRadius READ outerBottomRightRadius WRITE
setOuterBottomRightRadius NOTIFY outerBottomRightRadiusChanged)
public: public:
explicit BlobInvertedRect2(QQuickItem* parent = nullptr); explicit BlobInvertedRect2(QQuickItem* parent = nullptr);
@@ -40,13 +55,48 @@ class BlobInvertedRect2 : public BlobShape2 {
void setBorderBottom(qreal v); void setBorderBottom(qreal v);
[[nodiscard]] qreal outerRadius() const { return m_outerRadius; }
void setOuterRadius(qreal v);
[[nodiscard]] qreal outerTopLeftRadius() const {
return m_outerTopLeftRadius;
}
void setOuterTopLeftRadius(qreal v);
[[nodiscard]] qreal outerTopRightRadius() const {
return m_outerTopRightRadius;
}
void setOuterTopRightRadius(qreal v);
[[nodiscard]] qreal outerBottomLeftRadius() const {
return m_outerBottomLeftRadius;
}
void setOuterBottomLeftRadius(qreal v);
[[nodiscard]] qreal outerBottomRightRadius() const {
return m_outerBottomRightRadius;
}
void setOuterBottomRightRadius(qreal v);
signals: signals:
void borderLeftChanged(); void borderLeftChanged();
void borderRightChanged(); void borderRightChanged();
void borderTopChanged(); void borderTopChanged();
void borderBottomChanged(); void borderBottomChanged();
void outerRadiusChanged();
void outerTopLeftRadiusChanged();
void outerTopRightRadiusChanged();
void outerBottomLeftRadiusChanged();
void outerBottomRightRadiusChanged();
protected: protected:
void outerCornerRadii(float out[4]) const override;
void registerWithGroup() override; void registerWithGroup() override;
void unregisterFromGroup() override; void unregisterFromGroup() override;
@@ -55,4 +105,10 @@ class BlobInvertedRect2 : public BlobShape2 {
qreal m_borderRight = 0; qreal m_borderRight = 0;
qreal m_borderTop = 0; qreal m_borderTop = 0;
qreal m_borderBottom = 0; qreal m_borderBottom = 0;
qreal m_outerRadius = 0;
qreal m_outerTopLeftRadius = -1;
qreal m_outerTopRightRadius = -1;
qreal m_outerBottomLeftRadius = -1;
qreal m_outerBottomRightRadius = -1;
}; };
-15
View File
@@ -44,22 +44,17 @@ bool BlobMaterialShader::updateUniformData(
memcpy(buf->data() + 64, &opacity, 4); memcpy(buf->data() + 64, &opacity, 4);
} }
// Padded rect (offset 68)
memcpy(buf->data() + 68, &mat->m_paddedX, 4); memcpy(buf->data() + 68, &mat->m_paddedX, 4);
memcpy(buf->data() + 72, &mat->m_paddedY, 4); memcpy(buf->data() + 72, &mat->m_paddedY, 4);
memcpy(buf->data() + 76, &mat->m_paddedW, 4); memcpy(buf->data() + 76, &mat->m_paddedW, 4);
memcpy(buf->data() + 80, &mat->m_paddedH, 4); memcpy(buf->data() + 80, &mat->m_paddedH, 4);
// Smooth factor (offset 84)
memcpy(buf->data() + 84, &mat->m_smoothFactor, 4); memcpy(buf->data() + 84, &mat->m_smoothFactor, 4);
// Rect count (offset 88)
memcpy(buf->data() + 88, &mat->m_rectCount, 4); memcpy(buf->data() + 88, &mat->m_rectCount, 4);
// My index (offset 92). -2 skips the ownership test in the shader.
memcpy(buf->data() + 92, &mat->m_myIndex, 4); memcpy(buf->data() + 92, &mat->m_myIndex, 4);
// Color as vec4 (offset 96, 16 bytes)
const float color[4] = { const float color[4] = {
mat->m_color.redF(), mat->m_color.redF(),
mat->m_color.greenF(), mat->m_color.greenF(),
@@ -68,26 +63,19 @@ bool BlobMaterialShader::updateUniformData(
}; };
memcpy(buf->data() + 96, color, 16); memcpy(buf->data() + 96, color, 16);
// Has inverted (offset 112)
memcpy(buf->data() + 112, &mat->m_hasInverted, 4); memcpy(buf->data() + 112, &mat->m_hasInverted, 4);
// Inverted radius (offset 116)
memcpy(buf->data() + 116, &mat->m_invertedRadius, 4); memcpy(buf->data() + 116, &mat->m_invertedRadius, 4);
// Padding at 120-127 (skip)
// Inverted outer (offset 128, 16 bytes)
memcpy(buf->data() + 128, mat->m_invertedOuter, 16); memcpy(buf->data() + 128, mat->m_invertedOuter, 16);
// Inverted inner (offset 144, 16 bytes)
memcpy(buf->data() + 144, mat->m_invertedInner, 16); memcpy(buf->data() + 144, mat->m_invertedInner, 16);
// Rect data (offset 160, each rect = 5 vec4s = 80 bytes)
const int count = qMin(mat->m_rectCount, 16); const int count = qMin(mat->m_rectCount, 16);
for (int i = 0; i < count; ++i) { for (int i = 0; i < count; ++i) {
const auto& r = mat->m_rects[i]; const auto& r = mat->m_rects[i];
const int base = 160 + i * 80; const int base = 160 + i * 80;
// Pack excludeMask into props.x via bit-cast (read in shader with floatBitsToInt)
float maskAsFloat = NAN; float maskAsFloat = NAN;
memcpy(&maskAsFloat, &r.excludeMask, sizeof(float)); memcpy(&maskAsFloat, &r.excludeMask, sizeof(float));
const float d0[4] = {r.cx, r.cy, r.hw, r.hh}; const float d0[4] = {r.cx, r.cy, r.hw, r.hh};
@@ -100,12 +88,9 @@ bool BlobMaterialShader::updateUniformData(
memcpy(buf->data() + base + 64, r.radius, 16); memcpy(buf->data() + base + 64, r.radius, 16);
} }
// Shadow: after rectData, which ends at 1440. shadowColor is 16-byte aligned,
// so it lands at 1456.
memcpy(buf->data() + 1440, &mat->m_shadowRadius, 4); memcpy(buf->data() + 1440, &mat->m_shadowRadius, 4);
memcpy(buf->data() + 1456, mat->m_shadowColor, 16); memcpy(buf->data() + 1456, mat->m_shadowColor, 16);
// Outer edge corner radii, after shadowColor, at 1472.
memcpy(buf->data() + 1472, mat->m_invertedOuterRadii, 16); memcpy(buf->data() + 1472, mat->m_invertedOuterRadii, 16);
return true; return true;
-10
View File
@@ -8,14 +8,9 @@ struct BlobRectData {
float cx = 0, cy = 0, hw = 0, hh = 0; float cx = 0, cy = 0, hw = 0, hh = 0;
float offsetX = 0, offsetY = 0; float offsetX = 0, offsetY = 0;
float minEig = 1.0f; float minEig = 1.0f;
// Inverse of 2x2 deformation matrix, column-major for GLSL
float invDeform[4] = {1, 0, 0, 1}; float invDeform[4] = {1, 0, 0, 1};
// Screen-space AABB half-extents of the deformed rect
float screenHalfX = 0, screenHalfY = 0; float screenHalfX = 0, screenHalfY = 0;
// Effective per-corner radii (tr, br, bl, tl), pre-computed on CPU
float radius[4] = {0, 0, 0, 0}; float radius[4] = {0, 0, 0, 0};
// Bitmask of indices in this rect's m_cachedRects that mutually exclude (or are excluded by) this rect.
// Used by the shader to skip smin between excluded pairs.
int excludeMask = 0; int excludeMask = 0;
}; };
@@ -32,19 +27,14 @@ class BlobMaterial : public QSGMaterial {
float m_paddedH = 0; float m_paddedH = 0;
float m_smoothFactor = 32.0f; float m_smoothFactor = 32.0f;
int m_rectCount = 0; int m_rectCount = 0;
// -2 means no owner test: the single group draw writes every pixel it covers.
int m_myIndex = -2; int m_myIndex = -2;
QColor m_color{0x44, 0x88, 0xff}; QColor m_color{0x44, 0x88, 0xff};
int m_hasInverted = 0; int m_hasInverted = 0;
float m_invertedRadius = 0; float m_invertedRadius = 0;
float m_invertedOuter[4] = {}; float m_invertedOuter[4] = {};
float m_invertedInner[4] = {}; float m_invertedInner[4] = {};
// Phase 2: shadow around the material edges. Matches the uniform block, which
// puts these after rectData.
float m_shadowRadius = 0.0f; float m_shadowRadius = 0.0f;
float m_shadowColor[4] = {0.0f, 0.0f, 0.0f, 0.5f}; float m_shadowColor[4] = {0.0f, 0.0f, 0.0f, 0.5f};
// Outer-edge corner radii (tr, br, bl, tl). Matches the uniform block, which
// puts this after shadowColor.
float m_invertedOuterRadii[4] = {}; float m_invertedOuterRadii[4] = {};
BlobRectData m_rects[16] = {}; BlobRectData m_rects[16] = {};
}; };
+123 -81
View File
@@ -7,19 +7,94 @@
namespace { namespace {
constexpr float k_retireMs = 120.0f; constexpr float k_retireMs = 120.0f;
} constexpr qint64 k_minEvalMs = 10;
constexpr float k_staleSampleSec = 0.1f;
constexpr float k_staleMovePx = 2.0f;
constexpr float k_moveSpeed = 5.0f;
constexpr float k_settleDelta = 0.004f;
constexpr float k_settleVel = 0.05f;
constexpr float k_identityEps = 0.0005f;
constexpr float k_stillPx = 0.5f;
constexpr int k_settleStillSamples = 3;
} // namespace
BlobRect::BlobRect(QQuickItem* parent) : BlobShape(parent) {} BlobRect::BlobRect(QQuickItem* parent) : BlobShape(parent) {
m_stillSamples = k_settleStillSamples;
}
BlobRect::~BlobRect() { BlobRect::~BlobRect() {
if (m_group) m_group->removeShape(this); if (m_group) m_group->removeShape(this);
} }
bool BlobRect::needsFrame() const {
return m_physicsActive || m_retiring ||
m_stillSamples < k_settleStillSamples ||
std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) +
std::abs(m_dm11 - 1.0f) >
k_identityEps;
}
void BlobRect::resetDeformMatrix() {
m_dm00 = 1.0f;
m_dm01 = 0.0f;
m_dm11 = 1.0f;
m_dmVel00 = m_dmVel01 = m_dmVel11 = 0.0f;
m_retiring = false;
m_physicsActive = false;
BlobShape::resetDeformMatrix();
}
void BlobRect::requestPhysics() {
if (m_group) m_group->requestPhysics();
}
void BlobRect::updatePolish() { void BlobRect::updatePolish() {
BlobShape::updatePolish(); BlobShape::updatePolish();
if (m_physicsActive || m_retiring) requestPhysics();
}
void BlobRect::finishAtIdentity() {
m_dm00 = 1.0f;
m_dm01 = 0.0f;
m_dm11 = 1.0f;
m_deformMatrix = QMatrix4x4();
m_retiring = false;
m_physicsActive = false;
emit rawDeformMatrixChanged();
updateCenteredDeformMatrix();
if (m_group) m_group->markDirty();
}
void BlobRect::beginRetire() {
if (m_retiring) return;
m_retire00 = m_dm00;
m_retire01 = m_dm01;
m_retire11 = m_dm11;
m_dmVel00 = m_dmVel01 = m_dmVel11 = 0.0f;
m_retireElapsed.start();
m_retiring = true;
requestPhysics();
}
void BlobRect::trySettle(float speed) {
if (m_retiring) return;
if (!m_physicsActive) return; if (!m_physicsActive) return;
const float totalDelta =
std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) + std::abs(m_dm11 - 1.0f);
const float totalVel =
std::abs(m_dmVel00) + std::abs(m_dmVel01) + std::abs(m_dmVel11);
if (speed < k_moveSpeed && totalDelta < k_settleDelta &&
totalVel < k_settleVel && m_stillSamples >= k_settleStillSamples) {
beginRetire();
}
}
void BlobRect::updatePhysics() {
if (m_retiring) { if (m_retiring) {
const float t = std::clamp( const float t = std::clamp(
static_cast<float>(m_retireElapsed.elapsed()) / k_retireMs, static_cast<float>(m_retireElapsed.elapsed()) / k_retireMs,
@@ -28,100 +103,80 @@ void BlobRect::updatePolish() {
const float s = t * t * (3.0f - 2.0f * t); const float s = t * t * (3.0f - 2.0f * t);
if (t >= 1.0f) { if (t >= 1.0f) {
m_dm00 = 1.0f; finishAtIdentity();
m_dm01 = 0.0f; return;
m_dm11 = 1.0f;
m_deformMatrix = QMatrix4x4();
m_retiring = false;
m_physicsActive = false;
} else {
m_dm00 = m_retire00 + (1.0f - m_retire00) * s;
m_dm01 = m_retire01 - m_retire01 * s;
m_dm11 = m_retire11 + (1.0f - m_retire11) * s;
m_deformMatrix = QMatrix4x4(
m_dm00,
m_dm01,
0,
0,
m_dm01,
m_dm11,
0,
0,
0,
0,
1,
0,
0,
0,
0,
1);
} }
m_dm00 = m_retire00 + (1.0f - m_retire00) * s;
m_dm01 = m_retire01 - m_retire01 * s;
m_dm11 = m_retire11 + (1.0f - m_retire11) * s;
m_deformMatrix = QMatrix4x4(
m_dm00, m_dm01, 0, 0, m_dm01, m_dm11, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
emit rawDeformMatrixChanged(); emit rawDeformMatrixChanged();
updateCenteredDeformMatrix(); updateCenteredDeformMatrix();
if (!m_physicsActive) return;
} else {
const float totalDelta = std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) +
std::abs(m_dm11 - 1.0f);
const float totalVel =
std::abs(m_dmVel00) + std::abs(m_dmVel01) + std::abs(m_dmVel11);
if (totalDelta < 0.004f && totalVel < 0.05f) beginRetire();
} }
QMetaObject::invokeMethod(
this,
[this]() {
if (m_physicsActive && m_group) m_group->markDirty();
},
Qt::QueuedConnection);
}
void BlobRect::beginRetire() {
m_retire00 = m_dm00;
m_retire01 = m_dm01;
m_retire11 = m_dm11;
m_dmVel00 = m_dmVel01 = m_dmVel11 = 0.0f;
m_retireElapsed.start();
m_retiring = true;
}
void BlobRect::updatePhysics() {
const QPointF scenePos = mapToScene(QPointF(width() / 2.0, height() / 2.0)); const QPointF scenePos = mapToScene(QPointF(width() / 2.0, height() / 2.0));
if (!m_physicsActive && !m_retiring) {
const float stuck = std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) +
std::abs(m_dm11 - 1.0f);
if (stuck > k_identityEps) {
beginRetire();
return;
}
}
if (!m_hasPrevPos) { if (!m_hasPrevPos) {
m_prevScenePos = scenePos; m_prevScenePos = scenePos;
m_elapsed.start(); m_elapsed.start();
m_hasPrevPos = true; m_hasPrevPos = true;
m_stillSamples = k_settleStillSamples;
return; return;
} }
const float dt = static_cast<float>(m_elapsed.restart()) / 1000.0f; const qint64 evalMs = m_elapsed.elapsed();
if (dt > 0.1f || dt < 0.001f) { if (evalMs < k_minEvalMs) {
m_prevScenePos = scenePos;
if (m_physicsActive) checkAtRest(0.0f);
return; return;
} }
m_elapsed.restart();
const float velX = const float dx = static_cast<float>(scenePos.x() - m_prevScenePos.x());
static_cast<float>(scenePos.x() - m_prevScenePos.x()) / dt; const float dy = static_cast<float>(scenePos.y() - m_prevScenePos.y());
const float velY =
static_cast<float>(scenePos.y() - m_prevScenePos.y()) / dt;
m_prevScenePos = scenePos; m_prevScenePos = scenePos;
if (std::abs(dx) + std::abs(dy) > k_stillPx)
m_stillSamples = 0;
else
m_stillSamples += 1;
const float dt = static_cast<float>(evalMs) / 1000.0f;
if (dt > k_staleSampleSec) {
if (std::abs(dx) + std::abs(dy) > k_staleMovePx) {
m_physicsActive = true;
requestPhysics();
} else {
trySettle(0.0f);
}
return;
}
const float velX = dx / dt;
const float velY = dy / dt;
const float speed = std::sqrt(velX * velX + velY * velY); const float speed = std::sqrt(velX * velX + velY * velY);
if (m_retiring) { if (m_retiring) {
if (speed > 5.0f) if (speed > k_moveSpeed)
m_retiring = false; m_retiring = false;
else else
return; return;
} }
if (!m_physicsActive) { if (!m_physicsActive) {
if (speed < 5.0f) return; if (speed < k_moveSpeed) return;
m_physicsActive = true; m_physicsActive = true;
requestPhysics();
} }
const float kStretchFactor = static_cast<float>(m_deformScale); const float kStretchFactor = static_cast<float>(m_deformScale);
@@ -131,7 +186,7 @@ void BlobRect::updatePhysics() {
float target01 = 0.0f; float target01 = 0.0f;
float target11 = 1.0f; float target11 = 1.0f;
if (speed > 5.0f) { if (speed > k_moveSpeed) {
const float targetStretch = const float targetStretch =
1.0f + std::min(speed * kStretchFactor, kMaxStretch); 1.0f + std::min(speed * kStretchFactor, kMaxStretch);
const float targetCompress = 1.0f / targetStretch; const float targetCompress = 1.0f / targetStretch;
@@ -165,7 +220,7 @@ void BlobRect::updatePhysics() {
emit rawDeformMatrixChanged(); emit rawDeformMatrixChanged();
updateCenteredDeformMatrix(); updateCenteredDeformMatrix();
checkAtRest(speed); trySettle(speed);
} }
void BlobRect::setTopLeftRadius(qreal r) { void BlobRect::setTopLeftRadius(qreal r) {
@@ -340,16 +395,3 @@ void BlobRect::excludeCornersRemoveLast(QQmlListProperty<BlobRect>* prop) {
if (self->m_group) self->m_group->markDirty(); if (self->m_group) self->m_group->markDirty();
emit self->excludeCornersChanged(); emit self->excludeCornersChanged();
} }
void BlobRect::checkAtRest(float speed) {
if (m_retiring) return;
constexpr float kEpsilon = 0.002f;
const bool atRest =
std::abs(m_dm00 - 1.0f) < kEpsilon && std::abs(m_dm01) < kEpsilon &&
std::abs(m_dm11 - 1.0f) < kEpsilon && std::abs(m_dmVel00) < kEpsilon &&
std::abs(m_dmVel01) < kEpsilon && std::abs(m_dmVel11) < kEpsilon &&
speed < 5.0f;
if (atRest) beginRetire();
}
+6 -1
View File
@@ -105,15 +105,20 @@ class BlobRect : public BlobShape {
protected: protected:
void updatePolish() override; void updatePolish() override;
void updatePhysics() override; void updatePhysics() override;
bool needsFrame() const override;
void resetDeformMatrix() override;
private: private:
void checkAtRest(float speed); void trySettle(float speed);
void beginRetire(); void beginRetire();
void finishAtIdentity();
void requestPhysics();
QPointF m_prevScenePos; QPointF m_prevScenePos;
QElapsedTimer m_elapsed; QElapsedTimer m_elapsed;
bool m_physicsActive = false; bool m_physicsActive = false;
bool m_hasPrevPos = false; bool m_hasPrevPos = false;
int m_stillSamples = 0;
bool m_retiring = false; bool m_retiring = false;
QElapsedTimer m_retireElapsed; QElapsedTimer m_retireElapsed;
+109 -50
View File
@@ -6,14 +6,59 @@
namespace { namespace {
constexpr float k_retireMs = 120.0f; constexpr float k_retireMs = 120.0f;
constexpr qint64 k_minEvalMs = 10;
constexpr float k_staleSampleSec = 0.1f;
constexpr float k_staleMovePx = 2.0f;
constexpr float k_moveSpeed = 5.0f;
constexpr float k_settleDelta = 0.004f;
constexpr float k_settleVel = 0.05f;
constexpr float k_identityEps = 0.0005f;
constexpr float k_stillPx = 0.5f;
constexpr int k_settleStillSamples = 3;
} // namespace } // namespace
BlobRect2::BlobRect2(QQuickItem* parent) : BlobShape2(parent) {} BlobRect2::BlobRect2(QQuickItem* parent) : BlobShape2(parent) {
m_stillSamples = k_settleStillSamples;
}
BlobRect2::~BlobRect2() { BlobRect2::~BlobRect2() {
if (m_group) m_group->removeShape(this); if (m_group) m_group->removeShape(this);
} }
bool BlobRect2::needsFrame() const {
return m_physicsActive || m_retiring ||
m_stillSamples < k_settleStillSamples ||
std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) +
std::abs(m_dm11 - 1.0f) >
k_identityEps;
}
bool BlobRect2::isMoving() const {
return m_atRestOverride || m_physicsActive || m_retiring ||
m_stillSamples < k_settleStillSamples;
}
void BlobRect2::updateMoving() {
const bool moving = isMoving();
if (moving == m_movingEmitted) return;
m_movingEmitted = moving;
emit movingChanged();
}
void BlobRect2::resetDeformMatrix() {
m_dm00 = 1.0f;
m_dm01 = 0.0f;
m_dm11 = 1.0f;
m_dmVel00 = m_dmVel01 = m_dmVel11 = 0.0f;
m_retiring = false;
m_physicsActive = false;
BlobShape2::resetDeformMatrix();
}
void BlobRect2::requestPhysics() {
if (m_group) m_group->requestPhysics();
}
void BlobRect2::finishAtIdentity() { void BlobRect2::finishAtIdentity() {
m_dm00 = 1.0f; m_dm00 = 1.0f;
m_dm01 = 0.0f; m_dm01 = 0.0f;
@@ -23,11 +68,12 @@ void BlobRect2::finishAtIdentity() {
m_physicsActive = false; m_physicsActive = false;
emit rawDeformMatrixChanged(); emit rawDeformMatrixChanged();
updateCenteredDeformMatrix(); updateCenteredDeformMatrix();
if (m_group) m_group->markDirty();
updateMoving();
} }
void BlobRect2::beginRetire() { void BlobRect2::beginRetire() {
// The settle check would otherwise re-enter this every frame and restart the
// curve timer, so the curve never reaches t = 1.
if (m_retiring) return; if (m_retiring) return;
m_retire00 = m_dm00; m_retire00 = m_dm00;
@@ -36,10 +82,26 @@ void BlobRect2::beginRetire() {
m_dmVel00 = m_dmVel01 = m_dmVel11 = 0.0f; m_dmVel00 = m_dmVel01 = m_dmVel11 = 0.0f;
m_retireElapsed.start(); m_retireElapsed.start();
m_retiring = true; m_retiring = true;
requestPhysics();
updateMoving();
}
void BlobRect2::trySettle(float speed) {
if (m_retiring) return;
if (!m_physicsActive) return;
const float totalDelta =
std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) + std::abs(m_dm11 - 1.0f);
const float totalVel =
std::abs(m_dmVel00) + std::abs(m_dmVel01) + std::abs(m_dmVel11);
if (speed < k_moveSpeed && totalDelta < k_settleDelta &&
totalVel < k_settleVel && m_stillSamples >= k_settleStillSamples) {
beginRetire();
}
} }
void BlobRect2::updatePhysics() { void BlobRect2::updatePhysics() {
// Shapes are data only, so the group drives physics here.
if (m_retiring) { if (m_retiring) {
const float t = std::clamp( const float t = std::clamp(
static_cast<float>(m_retireElapsed.elapsed()) / k_retireMs, static_cast<float>(m_retireElapsed.elapsed()) / k_retireMs,
@@ -63,12 +125,16 @@ void BlobRect2::updatePhysics() {
if (!m_physicsActive) return; if (!m_physicsActive) return;
} }
// No "not active" guard here: an inactive shape still has to be evaluated so it if (!m_physicsActive && !m_retiring) {
// can see the panel start moving and become active. The frame loop is only kept const float stuck = std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) +
// alive while active, and it is requested before anything can return early — std::abs(m_dm11 - 1.0f);
// requesting it at the end lets a shape stop being evaluated with the matrix if (stuck > k_identityEps) {
// still off identity, which is what left the panel contents blurry. beginRetire();
if (m_physicsActive && m_group) m_group->markDirty(); return;
}
}
if (m_physicsActive) requestPhysics();
const QPointF scenePos = mapToScene(QPointF(width() / 2.0, height() / 2.0)); const QPointF scenePos = mapToScene(QPointF(width() / 2.0, height() / 2.0));
@@ -76,34 +142,54 @@ void BlobRect2::updatePhysics() {
m_prevScenePos = scenePos; m_prevScenePos = scenePos;
m_elapsed.start(); m_elapsed.start();
m_hasPrevPos = true; m_hasPrevPos = true;
m_stillSamples = k_settleStillSamples;
updateMoving();
return; return;
} }
const float dt = static_cast<float>(m_elapsed.restart()) / 1000.0f; const qint64 evalMs = m_elapsed.elapsed();
if (dt > 0.1f || dt < 0.001f) { if (evalMs < k_minEvalMs) {
m_prevScenePos = scenePos;
if (m_physicsActive) checkAtRest(0.0f);
return; return;
} }
m_elapsed.restart();
const float velX = const float dx = static_cast<float>(scenePos.x() - m_prevScenePos.x());
static_cast<float>(scenePos.x() - m_prevScenePos.x()) / dt; const float dy = static_cast<float>(scenePos.y() - m_prevScenePos.y());
const float velY =
static_cast<float>(scenePos.y() - m_prevScenePos.y()) / dt;
m_prevScenePos = scenePos; m_prevScenePos = scenePos;
if (std::abs(dx) + std::abs(dy) > k_stillPx)
m_stillSamples = 0;
else
m_stillSamples += 1;
const float dt = static_cast<float>(evalMs) / 1000.0f;
if (dt > k_staleSampleSec) {
if (std::abs(dx) + std::abs(dy) > k_staleMovePx) {
m_physicsActive = true;
requestPhysics();
updateMoving();
} else {
trySettle(0.0f);
}
return;
}
const float velX = dx / dt;
const float velY = dy / dt;
const float speed = std::sqrt(velX * velX + velY * velY); const float speed = std::sqrt(velX * velX + velY * velY);
if (m_retiring) { if (m_retiring) {
if (speed > 5.0f) if (speed > k_moveSpeed)
m_retiring = false; m_retiring = false;
else else
return; return;
} }
if (!m_physicsActive) { if (!m_physicsActive) {
if (speed < 5.0f) return; if (speed < k_moveSpeed) return;
m_physicsActive = true; m_physicsActive = true;
requestPhysics();
} }
const float kStretchFactor = static_cast<float>(m_deformScale); const float kStretchFactor = static_cast<float>(m_deformScale);
@@ -113,7 +199,7 @@ void BlobRect2::updatePhysics() {
float target01 = 0.0f; float target01 = 0.0f;
float target11 = 1.0f; float target11 = 1.0f;
if (speed > 5.0f) { if (speed > k_moveSpeed) {
const float targetStretch = const float targetStretch =
1.0f + std::min(speed * kStretchFactor, kMaxStretch); 1.0f + std::min(speed * kStretchFactor, kMaxStretch);
const float targetCompress = 1.0f / targetStretch; const float targetCompress = 1.0f / targetStretch;
@@ -147,22 +233,8 @@ void BlobRect2::updatePhysics() {
emit rawDeformMatrixChanged(); emit rawDeformMatrixChanged();
updateCenteredDeformMatrix(); updateCenteredDeformMatrix();
// Start the retire as soon as the matrix is close to identity, as in v1. Waiting trySettle(speed);
// for every term to fall below kEpsilon keeps the matrix just off identity for updateMoving();
// longer, and the panel content is transformed by it, which reads as blur on
// text.
const float totalDelta =
std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) + std::abs(m_dm11 - 1.0f);
const float totalVel =
std::abs(m_dmVel00) + std::abs(m_dmVel01) + std::abs(m_dmVel11);
// The residual is already inside the rest epsilon, so the eased curve would only
// be aborted or restarted every frame by the panel creeping. Snap instead.
if (totalDelta < 0.004f && totalVel < 0.05f) {
finishAtIdentity();
return;
}
checkAtRest(speed);
} }
void BlobRect2::setTopLeftRadius(qreal r) { void BlobRect2::setTopLeftRadius(qreal r) {
@@ -338,16 +410,3 @@ void BlobRect2::excludeCornersRemoveLast(QQmlListProperty<BlobRect2>* prop) {
if (self->m_group) self->m_group->markDirty(); if (self->m_group) self->m_group->markDirty();
emit self->excludeCornersChanged(); emit self->excludeCornersChanged();
} }
void BlobRect2::checkAtRest(float speed) {
if (m_retiring) return;
constexpr float kEpsilon = 0.002f;
const bool atRest =
std::abs(m_dm00 - 1.0f) < kEpsilon && std::abs(m_dm01) < kEpsilon &&
std::abs(m_dm11 - 1.0f) < kEpsilon && std::abs(m_dmVel00) < kEpsilon &&
std::abs(m_dmVel01) < kEpsilon && std::abs(m_dmVel11) < kEpsilon &&
speed < 5.0f;
if (atRest) beginRetire();
}
+12 -14
View File
@@ -25,6 +25,7 @@ class BlobRect2 : public BlobShape2 {
Q_PROPERTY( Q_PROPERTY(
bool atRestOverride READ atRestOverride WRITE setAtRestOverride NOTIFY bool atRestOverride READ atRestOverride WRITE setAtRestOverride NOTIFY
atRestOverrideChanged) atRestOverrideChanged)
Q_PROPERTY(bool moving READ isMoving NOTIFY movingChanged)
Q_PROPERTY( Q_PROPERTY(
qreal topLeftRadius READ topLeftRadius WRITE setTopLeftRadius NOTIFY qreal topLeftRadius READ topLeftRadius WRITE setTopLeftRadius NOTIFY
topLeftRadiusChanged) topLeftRadiusChanged)
@@ -78,18 +79,13 @@ class BlobRect2 : public BlobShape2 {
if (m_atRestOverride == o) return; if (m_atRestOverride == o) return;
m_atRestOverride = o; m_atRestOverride = o;
emit atRestOverrideChanged(); emit atRestOverrideChanged();
updateMoving();
if (m_group) m_group->markDirty(); if (m_group) m_group->markDirty();
} }
bool isExcluded(const BlobShape2* other) const override; bool isExcluded(const BlobShape2* other) const override;
bool isCornerExcluded(const BlobShape2* other) const override; bool isCornerExcluded(const BlobShape2* other) const override;
// The shape that is pushing is the one that can shape the bar edge. A shape that is bool isMoving() const override;
// retiring still counts, because the retire curve is still moving the surface.
// atRestOverride keeps the shape in the group's data even at rest; when it is false
// the actual spring state decides, so it is not an override of the state itself.
bool isMoving() const override {
return m_atRestOverride || m_physicsActive || m_retiring;
}
void cornerRadii(float out[4]) const override; void cornerRadii(float out[4]) const override;
[[nodiscard]] qreal topLeftRadius() const { return m_topLeftRadius; } [[nodiscard]] qreal topLeftRadius() const { return m_topLeftRadius; }
@@ -117,6 +113,7 @@ class BlobRect2 : public BlobShape2 {
void excludeChanged(); void excludeChanged();
void excludeCornersChanged(); void excludeCornersChanged();
void atRestOverrideChanged(); void atRestOverrideChanged();
void movingChanged();
void topLeftRadiusChanged(); void topLeftRadiusChanged();
void topRightRadiusChanged(); void topRightRadiusChanged();
void bottomLeftRadiusChanged(); void bottomLeftRadiusChanged();
@@ -124,20 +121,24 @@ class BlobRect2 : public BlobShape2 {
protected: protected:
void updatePhysics() override; void updatePhysics() override;
bool needsFrame() const override;
void resetDeformMatrix() override;
private: private:
void checkAtRest(float speed); void trySettle(float speed);
void beginRetire(); void beginRetire();
void finishAtIdentity(); void finishAtIdentity();
void requestPhysics();
void updateMoving();
// Physics state
QPointF m_prevScenePos; QPointF m_prevScenePos;
QElapsedTimer m_elapsed; QElapsedTimer m_elapsed;
bool m_physicsActive = false; bool m_physicsActive = false;
bool m_hasPrevPos = false; bool m_hasPrevPos = false;
// Set from QML. Keeps the shape in the group's data while it is at rest; when it is int m_stillSamples = 0;
// false the spring state decides on its own. bool m_movingEmitted = false;
bool m_atRestOverride = false; bool m_atRestOverride = false;
bool m_retiring = false; bool m_retiring = false;
@@ -146,13 +147,10 @@ class BlobRect2 : public BlobShape2 {
float m_retire01 = 0.0f; float m_retire01 = 0.0f;
float m_retire11 = 1.0f; float m_retire11 = 1.0f;
// Symmetric 2x2 deformation matrix components (3 independent: m00, m01,
// m11) Rest state is identity: m00=1, m01=0, m11=1
float m_dm00 = 1.0f; float m_dm00 = 1.0f;
float m_dm01 = 0.0f; float m_dm01 = 0.0f;
float m_dm11 = 1.0f; float m_dm11 = 1.0f;
// Spring velocities for each component
float m_dmVel00 = 0.0f; float m_dmVel00 = 0.0f;
float m_dmVel01 = 0.0f; float m_dmVel01 = 0.0f;
float m_dmVel11 = 0.0f; float m_dmVel11 = 0.0f;
+40 -15
View File
@@ -8,6 +8,10 @@
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
namespace {
constexpr int kMaxRects = 16;
} // namespace
static float deformPadding(const QMatrix4x4& dm, float hw, float hh) { static float deformPadding(const QMatrix4x4& dm, float hw, float hh) {
const float dm00 = dm(0, 0), dm01 = dm(0, 1); const float dm00 = dm(0, 0), dm01 = dm(0, 1);
const float dm10 = dm(1, 0), dm11 = dm(1, 1); const float dm10 = dm(1, 0), dm11 = dm(1, 1);
@@ -45,6 +49,7 @@ BlobShape::BlobShape(QQuickItem* parent) : QQuickItem(parent) {
void BlobShape::setGroup(BlobGroup* g) { void BlobShape::setGroup(BlobGroup* g) {
if (m_group == g) return; if (m_group == g) return;
if (m_group && isComponentComplete()) unregisterFromGroup(); if (m_group && isComponentComplete()) unregisterFromGroup();
if (m_group) resetDeformMatrix();
m_group = g; m_group = g;
if (m_group && isComponentComplete()) registerWithGroup(); if (m_group && isComponentComplete()) registerWithGroup();
emit groupChanged(); emit groupChanged();
@@ -87,6 +92,8 @@ void BlobShape::geometryChange(
std::abs(m_pendingDh) > syncThreshold) { std::abs(m_pendingDh) > syncThreshold) {
m_pendingDx = 0; m_pendingDx = 0;
m_pendingDy = 0; m_pendingDy = 0;
m_pendingDw = 0;
m_pendingDh = 0;
m_group->markShapeDirty(this); m_group->markShapeDirty(this);
} }
} }
@@ -105,6 +112,12 @@ void BlobShape::updateCenteredDeformMatrix() {
} }
} }
void BlobShape::resetDeformMatrix() {
m_deformMatrix = QMatrix4x4();
emit rawDeformMatrixChanged();
updateCenteredDeformMatrix();
}
void BlobShape::cornerRadii(float out[4]) const { void BlobShape::cornerRadii(float out[4]) const {
const auto maxR = static_cast<float>(std::min(width(), height())) * 0.5f; const auto maxR = static_cast<float>(std::min(width(), height())) * 0.5f;
const auto r = std::min(static_cast<float>(m_radius), maxR); const auto r = std::min(static_cast<float>(m_radius), maxR);
@@ -187,6 +200,12 @@ void BlobShape::updatePolish() {
} }
if (include) { if (include) {
if (other == this && m_cachedRects.size() >= kMaxRects) {
m_cachedRects.removeLast();
rectShapes.removeLast();
}
if (m_cachedRects.size() >= kMaxRects) continue;
if (other == this) if (other == this)
m_cachedMyIndex = static_cast<int>(m_cachedRects.size()); m_cachedMyIndex = static_cast<int>(m_cachedRects.size());
@@ -240,8 +259,7 @@ void BlobShape::updatePolish() {
m_cachedInvertedRadius = 0; m_cachedInvertedRadius = 0;
memset(m_cachedInvertedOuter, 0, sizeof(m_cachedInvertedOuter)); memset(m_cachedInvertedOuter, 0, sizeof(m_cachedInvertedOuter));
memset(m_cachedInvertedInner, 0, sizeof(m_cachedInvertedInner)); memset(m_cachedInvertedInner, 0, sizeof(m_cachedInvertedInner));
memset( memset(m_cachedInvertedOuterRadii, 0, sizeof(m_cachedInvertedOuterRadii));
m_cachedInvertedOuterRadii, 0, sizeof(m_cachedInvertedOuterRadii));
auto* inv = m_group->invertedRect(); auto* inv = m_group->invertedRect();
if (inv) { if (inv) {
@@ -308,10 +326,11 @@ void BlobShape::updatePolish() {
BlobShape* const si = rectShapes[i]; BlobShape* const si = rectShapes[i];
float fTr = 1.0f, fBr = 1.0f, fBl = 1.0f, fTl = 1.0f; float fTr = 1.0f, fBr = 1.0f, fBl = 1.0f, fTl = 1.0f;
const float cTrX = ri.cx + ri.hw, cTrY = ri.cy - ri.hh; const float dcx = ri.cx + ri.offsetX, dcy = ri.cy + ri.offsetY;
const float cBrX = ri.cx + ri.hw, cBrY = ri.cy + ri.hh; const float cTrX = dcx + ri.hw, cTrY = dcy - ri.hh;
const float cBlX = ri.cx - ri.hw, cBlY = ri.cy + ri.hh; const float cBrX = dcx + ri.hw, cBrY = dcy + ri.hh;
const float cTlX = ri.cx - ri.hw, cTlY = ri.cy - ri.hh; const float cBlX = dcx - ri.hw, cBlY = dcy + ri.hh;
const float cTlX = dcx - ri.hw, cTlY = dcy - ri.hh;
for (qsizetype j = 0; cornerFill && j < rectCount; ++j) { for (qsizetype j = 0; cornerFill && j < rectCount; ++j) {
if (j == i) continue; if (j == i) continue;
@@ -319,10 +338,12 @@ void BlobShape::updatePolish() {
BlobShape* const sj = rectShapes[j]; BlobShape* const sj = rectShapes[j];
if (si->isCornerExcluded(sj) || sj->isCornerExcluded(si)) continue; if (si->isCornerExcluded(sj) || sj->isCornerExcluded(si)) continue;
const auto& rj = m_cachedRects[j]; const auto& rj = m_cachedRects[j];
const float sdTr = cpuSdBox(cTrX, cTrY, rj.cx, rj.cy, rj.hw, rj.hh); const float jcx = rj.cx + rj.offsetX;
const float sdBr = cpuSdBox(cBrX, cBrY, rj.cx, rj.cy, rj.hw, rj.hh); const float jcy = rj.cy + rj.offsetY;
const float sdBl = cpuSdBox(cBlX, cBlY, rj.cx, rj.cy, rj.hw, rj.hh); const float sdTr = cpuSdBox(cTrX, cTrY, jcx, jcy, rj.hw, rj.hh);
const float sdTl = cpuSdBox(cTlX, cTlY, rj.cx, rj.cy, rj.hw, rj.hh); const float sdBr = cpuSdBox(cBrX, cBrY, jcx, jcy, rj.hw, rj.hh);
const float sdBl = cpuSdBox(cBlX, cBlY, jcx, jcy, rj.hw, rj.hh);
const float sdTl = cpuSdBox(cTlX, cTlY, jcx, jcy, rj.hw, rj.hh);
fTr = std::min(fTr, cornerFillFactor(sdTr, smoothFactor)); fTr = std::min(fTr, cornerFillFactor(sdTr, smoothFactor));
fBr = std::min(fBr, cornerFillFactor(sdBr, smoothFactor)); fBr = std::min(fBr, cornerFillFactor(sdBr, smoothFactor));
fBl = std::min(fBl, cornerFillFactor(sdBl, smoothFactor)); fBl = std::min(fBl, cornerFillFactor(sdBl, smoothFactor));
@@ -360,10 +381,14 @@ void BlobShape::updatePolish() {
-cpuSdBox(cTlX, cTlY, icx, icy, ihw, ihh))); -cpuSdBox(cTlX, cTlY, icx, icy, ihw, ihh)));
} }
ri.radius[0] = std::max(ri.radius[0] * fTr, minR); ri.radius[0] = ri.radius[0] > 0.0f ? std::max(ri.radius[0] * fTr, minR)
ri.radius[1] = std::max(ri.radius[1] * fBr, minR); : 0.0f;
ri.radius[2] = std::max(ri.radius[2] * fBl, minR); ri.radius[1] = ri.radius[1] > 0.0f ? std::max(ri.radius[1] * fBr, minR)
ri.radius[3] = std::max(ri.radius[3] * fTl, minR); : 0.0f;
ri.radius[2] = ri.radius[2] > 0.0f ? std::max(ri.radius[2] * fBl, minR)
: 0.0f;
ri.radius[3] = ri.radius[3] > 0.0f ? std::max(ri.radius[3] * fTl, minR)
: 0.0f;
} }
} }
@@ -428,7 +453,7 @@ QSGNode* BlobShape::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) {
sizeof(m_cachedInvertedOuterRadii)); sizeof(m_cachedInvertedOuterRadii));
const int count = const int count =
static_cast<int>(qMin(m_cachedRects.size(), qsizetype(16))); static_cast<int>(qMin(m_cachedRects.size(), qsizetype(kMaxRects)));
material->m_rectCount = count; material->m_rectCount = count;
for (int i = 0; i < count; ++i) for (int i = 0; i < count; ++i)
material->m_rects[i] = m_cachedRects[i]; material->m_rects[i] = m_cachedRects[i];
+4 -4
View File
@@ -61,9 +61,6 @@ class BlobShape : public QQuickItem {
virtual void cornerRadii(float out[4]) const; virtual void cornerRadii(float out[4]) const;
// Outer-edge corner radii. Only the inverted rect draws an outer frame edge,
// so the base has no outer rounding and returns zeros. Subclasses that expose
// outer corner properties override this (order: tr, br, bl, tl).
virtual void outerCornerRadii(float out[4]) const { virtual void outerCornerRadii(float out[4]) const {
out[0] = 0; out[0] = 0;
out[1] = 0; out[1] = 0;
@@ -73,16 +70,19 @@ class BlobShape : public QQuickItem {
virtual void updatePhysics() {} virtual void updatePhysics() {}
virtual bool needsFrame() const { return false; }
virtual void registerWithGroup(); virtual void registerWithGroup();
virtual void unregisterFromGroup(); virtual void unregisterFromGroup();
void updateCenteredDeformMatrix(); void updateCenteredDeformMatrix();
virtual void resetDeformMatrix();
BlobGroup* m_group = nullptr; BlobGroup* m_group = nullptr;
qreal m_radius = 0; qreal m_radius = 0;
QMatrix4x4 m_deformMatrix; // identity by default QMatrix4x4 m_deformMatrix; // identity by default
QMatrix4x4 m_centeredDeformMatrix; QMatrix4x4 m_centeredDeformMatrix;
// Cached data from updatePolish
float m_cachedPaddedX = 0; float m_cachedPaddedX = 0;
float m_cachedPaddedY = 0; float m_cachedPaddedY = 0;
float m_cachedPaddedW = 0; float m_cachedPaddedW = 0;
+7 -1
View File
@@ -9,6 +9,7 @@ BlobShape2::BlobShape2(QQuickItem* parent) : QQuickItem(parent) {}
void BlobShape2::setGroup(BlobGroup2* g) { void BlobShape2::setGroup(BlobGroup2* g) {
if (m_group == g) return; if (m_group == g) return;
if (m_group && isComponentComplete()) unregisterFromGroup(); if (m_group && isComponentComplete()) unregisterFromGroup();
if (m_group) resetDeformMatrix();
m_group = g; m_group = g;
if (m_group && isComponentComplete()) registerWithGroup(); if (m_group && isComponentComplete()) registerWithGroup();
emit groupChanged(); emit groupChanged();
@@ -32,7 +33,6 @@ void BlobShape2::geometryChange(
QQuickItem::geometryChange(newGeometry, oldGeometry); QQuickItem::geometryChange(newGeometry, oldGeometry);
updateCenteredDeformMatrix(); updateCenteredDeformMatrix();
// One draw covers the whole group, so any shape change is a group change.
if (m_group) m_group->markDirty(); if (m_group) m_group->markDirty();
} }
@@ -49,6 +49,12 @@ void BlobShape2::updateCenteredDeformMatrix() {
} }
} }
void BlobShape2::resetDeformMatrix() {
m_deformMatrix = QMatrix4x4();
emit rawDeformMatrixChanged();
updateCenteredDeformMatrix();
}
void BlobShape2::cornerRadii(float out[4]) const { void BlobShape2::cornerRadii(float out[4]) const {
const auto maxR = static_cast<float>(std::min(width(), height())) * 0.5f; const auto maxR = static_cast<float>(std::min(width(), height())) * 0.5f;
const auto r = std::min(static_cast<float>(m_radius), maxR); const auto r = std::min(static_cast<float>(m_radius), maxR);
+11 -9
View File
@@ -7,12 +7,6 @@
class BlobGroup2; class BlobGroup2;
// Data-only shape.
//
// The group draws a single node for the whole group, so a shape here contributes
// only what the shader needs: position, radii, exclusion, physics. A shape does
// not create a render node, so no pixel can be evaluated by one shape and missed
// by another.
class BlobShape2 : public QQuickItem { class BlobShape2 : public QQuickItem {
Q_OBJECT Q_OBJECT
Q_PROPERTY(BlobGroup2* group READ group WRITE setGroup NOTIFY groupChanged) Q_PROPERTY(BlobGroup2* group READ group WRITE setGroup NOTIFY groupChanged)
@@ -56,8 +50,6 @@ class BlobShape2 : public QQuickItem {
virtual bool isExcluded(const BlobShape2*) const { return false; } virtual bool isExcluded(const BlobShape2*) const { return false; }
// Whether this shape is part of the group's data. Only a pushing shape is, so a shape
// at rest has no part in the draw. BlobRect2 also honours the QML atRestOverride.
virtual bool isMoving() const { return false; } virtual bool isMoving() const { return false; }
virtual bool isCornerExcluded(const BlobShape2* /*other*/) const { virtual bool isCornerExcluded(const BlobShape2* /*other*/) const {
@@ -66,13 +58,23 @@ class BlobShape2 : public QQuickItem {
virtual void cornerRadii(float out[4]) const; virtual void cornerRadii(float out[4]) const;
// Called by the group when it rebuilds the shared data, once per frame. virtual void outerCornerRadii(float out[4]) const {
out[0] = 0;
out[1] = 0;
out[2] = 0;
out[3] = 0;
}
virtual void updatePhysics() {} virtual void updatePhysics() {}
virtual bool needsFrame() const { return false; }
virtual void registerWithGroup(); virtual void registerWithGroup();
virtual void unregisterFromGroup(); virtual void unregisterFromGroup();
void updateCenteredDeformMatrix(); void updateCenteredDeformMatrix();
virtual void resetDeformMatrix();
BlobGroup2* m_group = nullptr; BlobGroup2* m_group = nullptr;
qreal m_radius = 0; qreal m_radius = 0;
QMatrix4x4 m_deformMatrix; // identity by default QMatrix4x4 m_deformMatrix; // identity by default
+35 -94
View File
@@ -30,7 +30,6 @@ float sdRoundedBox(vec2 p, vec2 center, vec2 halfSize, float radius) {
} }
float sdRoundedBox4(vec2 p, vec2 center, vec2 halfSize, vec4 r) { float sdRoundedBox4(vec2 p, vec2 center, vec2 halfSize, vec4 r) {
// r = (topRight, bottomRight, bottomLeft, topLeft)
p -= center; p -= center;
r.xy = (p.x > 0.0) ? r.xy : r.wz; r.xy = (p.x > 0.0) ? r.xy : r.wz;
r.x = (p.y > 0.0) ? r.y : r.x; r.x = (p.y > 0.0) ? r.y : r.x;
@@ -44,7 +43,6 @@ float sdBox(vec2 p, vec2 center, vec2 halfSize) {
} }
float smin(float a, float b, float k) { float smin(float a, float b, float k) {
// Cubic smooth min (C2 continuous — no curvature kinks at blend boundary)
float h = max(k - abs(a - b), 0.0) / k; float h = max(k - abs(a - b), 0.0) / k;
return min(a, b) - h * h * h * k * (1.0 / 6.0); return min(a, b) - h * h * h * k * (1.0 / 6.0);
} }
@@ -55,9 +53,6 @@ float smax(float a, float b, float k) {
} }
float smaxSharpA(float a, float b, float k) { float smaxSharpA(float a, float b, float k) {
// smax variant that keeps a's boundary sharp (no inward rounding at a = 0).
// Used for the frame inner side, so the outer edge keeps its own shape and
// always fills to the edges.
float h = max(k - abs(a - b), 0.0) / k; float h = max(k - abs(a - b), 0.0) / k;
float blend = h * h * h * k * (1.0 / 6.0); float blend = h * h * h * k * (1.0 / 6.0);
blend *= smoothstep(0.0, k * 0.5, -a); blend *= smoothstep(0.0, k * 0.5, -a);
@@ -67,10 +62,6 @@ float smaxSharpA(float a, float b, float k) {
void main() { void main() {
vec2 pixel = vec2(paddedX, paddedY) + qt_TexCoord0 * vec2(paddedW, paddedH); vec2 pixel = vec2(paddedX, paddedY) + qt_TexCoord0 * vec2(paddedW, paddedH);
// Phase 1: compute per-rect SDF, and track which rect owns the pixel for the
// per-shape path (BlobGroup). The single group draw (BlobGroup2) has no owner:
// myIndex stays -2, so the ownership test is skipped and this pass writes every
// pixel the quad covers. Excluded pairs still need the pairwise pass below.
float dArr[16]; float dArr[16];
int owner = -2; int owner = -2;
float minDist = 1e10; float minDist = 1e10;
@@ -84,12 +75,8 @@ void main() {
vec4 radii = vec4 radii =
rectData[i * 5 + 4]; // effective per-corner radii (tr, br, bl, tl) rectData[i * 5 + 4]; // effective per-corner radii (tr, br, bl, tl)
// Offset center for asymmetric deformation
vec2 center = rect.xy + props.yz; vec2 center = rect.xy + props.yz;
// AABB early-out: skip rects far from this pixel. The reach has to include the
// shadow, otherwise a rect that is only inside the shadow range would be
// skipped and its distance could not be used.
vec2 extent = sh.xy + vec2(smoothFactor * 1.5 + shadowRadius); vec2 extent = sh.xy + vec2(smoothFactor * 1.5 + shadowRadius);
if (abs(pixel.x - center.x) > extent.x || if (abs(pixel.x - center.x) > extent.x ||
abs(pixel.y - center.y) > extent.y) { abs(pixel.y - center.y) > extent.y) {
@@ -97,19 +84,13 @@ void main() {
continue; continue;
} }
// Apply pre-computed inverse deformation to the evaluation point
mat2 invDeform = mat2(invDm.xy, invDm.zw); mat2 invDeform = mat2(invDm.xy, invDm.zw);
vec2 transformedPixel = center + invDeform * (pixel - center); vec2 transformedPixel = center + invDeform * (pixel - center);
// Use pre-computed effective per-corner radii
float d = sdRoundedBox4(transformedPixel, center, rect.zw, radii); float d = sdRoundedBox4(transformedPixel, center, rect.zw, radii);
// Use pre-computed minimum eigenvalue for SDF correction
d *= max(props.w, 0.01); d *= max(props.w, 0.01);
// Scale SDF on the axis facing a nearby border to narrow the smin blend
// zone in that direction only, without reducing k (which would cause sharp
// edges).
if (hasInverted != 0) { if (hasInverted != 0) {
vec2 screenHalf = sh.xy; vec2 screenHalf = sh.xy;
@@ -122,26 +103,21 @@ void main() {
float distX1 = float distX1 =
(invertedInner.x + invertedInner.z) - (center.x - screenHalf.x); (invertedInner.x + invertedInner.z) - (center.x - screenHalf.x);
// 0 = far from border, 1 = at border (max compression)
float yProx = 1.0 - min(smoothstep(0.0, smoothFactor, distY0), float yProx = 1.0 - min(smoothstep(0.0, smoothFactor, distY0),
smoothstep(0.0, smoothFactor, distY1)); smoothstep(0.0, smoothFactor, distY1));
float xProx = 1.0 - min(smoothstep(0.0, smoothFactor, distX0), float xProx = 1.0 - min(smoothstep(0.0, smoothFactor, distX0),
smoothstep(0.0, smoothFactor, distX1)); smoothstep(0.0, smoothFactor, distX1));
// Smooth axis weights: gradient-based at corners, face-based inside.
vec2 q = abs(pixel - center) - screenHalf; vec2 q = abs(pixel - center) - screenHalf;
vec2 qp = max(q, vec2(0.0)); vec2 qp = max(q, vec2(0.0));
float cornerLen = length(qp); float cornerLen = length(qp);
// Gradient direction in corner region (smooth 90-degree rotation)
float gradX = qp.x / max(cornerLen, 0.001); float gradX = qp.x / max(cornerLen, 0.001);
float gradY = qp.y / max(cornerLen, 0.001); float gradY = qp.y / max(cornerLen, 0.001);
// Smooth face weights for inside/edge (no hard branch)
float faceY = smoothstep(-4.0, 4.0, q.y - q.x); float faceY = smoothstep(-4.0, 4.0, q.y - q.x);
float faceX = 1.0 - faceY; float faceX = 1.0 - faceY;
// Blend: gradient in corner region, face-based inside
float t = smoothstep(0.0, 2.0, cornerLen); float t = smoothstep(0.0, 2.0, cornerLen);
float xWeight = mix(faceX, gradX, t); float xWeight = mix(faceX, gradX, t);
float yWeight = mix(faceY, gradY, t); float yWeight = mix(faceY, gradY, t);
@@ -158,15 +134,11 @@ void main() {
} }
} }
// Phase 2: hard-min baseline over all rects.
float mergedSdf = 1e10; float mergedSdf = 1e10;
for (int i = 0; i < rectCount; i++) { for (int i = 0; i < rectCount; i++) {
mergedSdf = min(mergedSdf, dArr[i]); mergedSdf = min(mergedSdf, dArr[i]);
} }
// Phase 3: pair-wise smin contributions, skipping excluded pairs. Pair smin
// <= min, so taking the min over all non-excluded pair smins gives the
// smoothly-merged SDF.
for (int i = 0; i < rectCount; i++) { for (int i = 0; i < rectCount; i++) {
if (dArr[i] >= 1e9) if (dArr[i] >= 1e9)
continue; continue;
@@ -176,7 +148,6 @@ void main() {
continue; continue;
if ((excludeMask & (1 << j)) != 0) if ((excludeMask & (1 << j)) != 0)
continue; continue;
// smin only deviates from min within smoothFactor
if (abs(dArr[i] - dArr[j]) >= smoothFactor) if (abs(dArr[i] - dArr[j]) >= smoothFactor)
continue; continue;
mergedSdf = min(mergedSdf, smin(dArr[i], dArr[j], smoothFactor)); mergedSdf = min(mergedSdf, smin(dArr[i], dArr[j], smoothFactor));
@@ -184,16 +155,12 @@ void main() {
} }
if (hasInverted != 0) { if (hasInverted != 0) {
// Outer edge rounds with its own per-corner radii. With all-zero radii
// sdRoundedBox4 equals sdBox, so a frame that sets no outer radius is
// unchanged. smaxSharpA keeps this boundary authoritative for the fill.
float dOuter = float dOuter =
sdRoundedBox4(pixel, invertedOuter.xy, invertedOuter.zw, sdRoundedBox4(pixel, invertedOuter.xy, invertedOuter.zw,
invertedOuterRadii) - 1.0; invertedOuterRadii) - 1.0;
float dInner = float dInner =
sdRoundedBox(pixel, invertedInner.xy, invertedInner.zw, invertedRadius); sdRoundedBox(pixel, invertedInner.xy, invertedInner.zw, invertedRadius);
// Border sinks: track the opposite rect edge, clamped to border thickness
float innerTop = invertedInner.y - invertedInner.w; float innerTop = invertedInner.y - invertedInner.w;
float innerBot = invertedInner.y + invertedInner.w; float innerBot = invertedInner.y + invertedInner.w;
float innerLeft = invertedInner.x - invertedInner.z; float innerLeft = invertedInner.x - invertedInner.z;
@@ -203,62 +170,51 @@ void main() {
float outerLeft = invertedOuter.x - invertedOuter.z; float outerLeft = invertedOuter.x - invertedOuter.z;
float outerRight = invertedOuter.x + invertedOuter.z; float outerRight = invertedOuter.x + invertedOuter.z;
float topZone =
1.0 - smoothstep(innerTop, innerTop + smoothFactor, pixel.y);
float botZone = smoothstep(innerBot - smoothFactor, innerBot, pixel.y);
float leftZone =
1.0 - smoothstep(innerLeft, innerLeft + smoothFactor, pixel.x);
float rightZone =
smoothstep(innerRight - smoothFactor, innerRight, pixel.x);
float sinkValue = 0.0; float sinkValue = 0.0;
for (int i = 0; i < rectCount; i++) { float preOff = smoothFactor * (1.0 / 6.0);
vec4 rect = rectData[i * 5]; float s = smoothFactor * 2.0;
vec4 sinkProps = rectData[i * 5 + 1];
vec2 sinkSh = rectData[i * 5 + 3].xy;
// Screen-space center (with offset) and pre-computed AABB half-extents if (topZone > 0.0 || botZone > 0.0 || leftZone > 0.0 || rightZone > 0.0) {
vec2 ctr = rect.xy + sinkProps.yz; for (int i = 0; i < rectCount; i++) {
vec4 rect = rectData[i * 5];
vec4 sinkProps = rectData[i * 5 + 1];
vec2 sinkSh = rectData[i * 5 + 3].xy;
// Delay sink to absorb smin blend depth (cubic smin max = k/6) vec2 ctr = rect.xy + sinkProps.yz;
float preOff = smoothFactor * (1.0 / 6.0);
// Top border: track rect's BOTTOM edge, only within border thickness float topPen = clamp(innerTop - (ctr.y + sinkSh.y) - preOff, 0.0,
float topPen = clamp(innerTop - (ctr.y + sinkSh.y) - preOff, 0.0, innerTop - outerTop);
innerTop - outerTop);
// Bottom border: track rect's TOP edge float botPen = clamp((ctr.y - sinkSh.y) - innerBot - preOff, 0.0,
float botPen = clamp((ctr.y - sinkSh.y) - innerBot - preOff, 0.0, outerBot - innerBot);
outerBot - innerBot);
// Left border: track rect's RIGHT edge float leftPen = clamp(innerLeft - (ctr.x + sinkSh.x) - preOff, 0.0,
float leftPen = clamp(innerLeft - (ctr.x + sinkSh.x) - preOff, 0.0, innerLeft - outerLeft);
innerLeft - outerLeft);
// Right border: track rect's LEFT edge float rightPen = clamp((ctr.x - sinkSh.x) - innerRight - preOff, 0.0,
float rightPen = clamp((ctr.x - sinkSh.x) - innerRight - preOff, 0.0, outerRight - innerRight);
outerRight - innerRight);
// Lateral distance from pixel to rect's extent along each edge float hLat = max(abs(pixel.x - ctr.x) - sinkSh.x, 0.0);
float hLat = max(abs(pixel.x - ctr.x) - sinkSh.x, 0.0); float vLat = max(abs(pixel.y - ctr.y) - sinkSh.y, 0.0);
float vLat = max(abs(pixel.y - ctr.y) - sinkSh.y, 0.0);
// Perpendicular proximity: full strength in border, fade inside inner float sink = max(max(topPen * smoothstep(s, 0.0, hLat) * topZone,
// area botPen * smoothstep(s, 0.0, hLat) * botZone),
float topZone = max(leftPen * smoothstep(s, 0.0, vLat) * leftZone,
1.0 - smoothstep(innerTop, innerTop + smoothFactor, pixel.y); rightPen * smoothstep(s, 0.0, vLat) * rightZone));
float botZone = smoothstep(innerBot - smoothFactor, innerBot, pixel.y); sinkValue = max(sinkValue, sink);
float leftZone = }
1.0 - smoothstep(innerLeft, innerLeft + smoothFactor, pixel.x);
float rightZone =
smoothstep(innerRight - smoothFactor, innerRight, pixel.x);
float s = smoothFactor * 2.0;
float sink = max(max(topPen * smoothstep(s, 0.0, hLat) * topZone,
botPen * smoothstep(s, 0.0, hLat) * botZone),
max(leftPen * smoothstep(s, 0.0, vLat) * leftZone,
rightPen * smoothstep(s, 0.0, vLat) * rightZone));
sinkValue = max(sinkValue, sink);
} }
dInner -= sinkValue; dInner -= sinkValue;
// dOuter is already a true circle (sdRoundedBox4). Keep it exact whenever the
// outer arc is the active boundary; a smax term added there would pull the
// arc's sides inward (curvature/tangent mismatch), so the corner would not be
// a circle. Only blend on the inner side, where the inner boundary is active.
float negInner = -dInner; float negInner = -dInner;
float dFrame = dOuter >= negInner ? dOuter float dFrame = dOuter >= negInner ? dOuter
: smaxSharpA(dOuter, negInner, smoothFactor); : smaxSharpA(dOuter, negInner, smoothFactor);
@@ -269,37 +225,22 @@ void main() {
} }
} }
// A pixel this far from every rect cannot have a value, so it is not written.
// The one-unit margin keeps the outermost pixels of the quad out of the AA: their
// derivatives are undefined (the quad neighbour is outside the primitive), which
// is what drew a line along the coverage edge.
float reach = max(smoothFactor, shadowRadius); float reach = max(smoothFactor, shadowRadius);
if (mergedSdf > reach - 1.0) float pxWidth = max(length(vec2(dFdx(mergedSdf), dFdy(mergedSdf))), 1e-5);
if (mergedSdf > reach - max(2.0 * pxWidth, 1.0))
discard; discard;
// Only the owner writes a pixel, which is what makes the per-shape path look
// like one surface: qt_Opacity is applied once per pixel instead of once per
// overlapping draw. The single group draw has no owner (myIndex == -2), so the
// test is skipped and the value below is the only value a pixel can have.
if (myIndex >= -1 && owner != myIndex) if (myIndex >= -1 && owner != myIndex)
discard; discard;
// Fixed-width AA: fwidth is capped at one unit, so a bad derivative near the quad float materialAlpha = clamp(0.5 - mergedSdf / pxWidth, 0.0, 1.0);
// boundary cannot make a wide edge.
float fw = min(fwidth(mergedSdf), 1.0);
float materialAlpha = 1.0 - smoothstep(-fw, fw, mergedSdf);
// Phase 2: the shadow, as a distance falloff from the merged surface. The
// quadratic reaches zero with zero slope at its outer edge, so that edge needs no
// AA of its own. Only the single group draw writes a shadow: in the per-shape
// path overlapping draws would darken the same area twice.
float shadowAlpha = 0.0; float shadowAlpha = 0.0;
if (shadowRadius > 0.0 && myIndex == -2) { if (shadowRadius > 0.0 && myIndex == -2) {
float t = clamp(mergedSdf / shadowRadius, 0.0, 1.0); float t = clamp(mergedSdf / shadowRadius, 0.0, 1.0);
shadowAlpha = (1.0 - t) * (1.0 - t) * shadowColor.a; shadowAlpha = (1.0 - t) * (1.0 - t) * shadowColor.a;
} }
// Material over shadow, premultiplied.
float totalAlpha = materialAlpha + shadowAlpha * (1.0 - materialAlpha); float totalAlpha = materialAlpha + shadowAlpha * (1.0 - materialAlpha);
vec3 rgb = color.rgb * materialAlpha vec3 rgb = color.rgb * materialAlpha
+ shadowColor.rgb * shadowAlpha * (1.0 - materialAlpha); + shadowColor.rgb * shadowAlpha * (1.0 - materialAlpha);
Binary file not shown.
+1
View File
@@ -47,6 +47,7 @@ list(APPEND CMAKE_AUTOMOC_MACRO_NAMES
add_subdirectory(Settings) add_subdirectory(Settings)
add_subdirectory(Models) add_subdirectory(Models)
add_subdirectory(Internal) add_subdirectory(Internal)
add_subdirectory(Wallpaper)
add_subdirectory(Services) add_subdirectory(Services)
add_subdirectory(Components) add_subdirectory(Components)
add_subdirectory(Blobs) add_subdirectory(Blobs)
+138 -46
View File
@@ -4,6 +4,7 @@
#include <QQuickWindow> #include <QQuickWindow>
#include <QScopeGuard> #include <QScopeGuard>
#include <QtGui/private/qfont_p.h> #include <QtGui/private/qfont_p.h>
#include <algorithm>
#include <cmath> #include <cmath>
Q_LOGGING_CATEGORY(lcQuickLayouts, "qt.quick.layouts") Q_LOGGING_CATEGORY(lcQuickLayouts, "qt.quick.layouts")
@@ -54,49 +55,13 @@ SnapGridLayoutItem::SnapGridLayoutItem(
int column, int column,
int rowSpan, int rowSpan,
int columnSpan, int columnSpan,
Qt::Alignment alignment, Qt::Alignment alignment)
SnapLayout* layout) : QQuickGridLayoutItem(item, row, column, rowSpan, columnSpan, alignment) {}
: QQuickGridLayoutItem(item, row, column, rowSpan, columnSpan, alignment)
, m_layout(layout) {}
void SnapGridLayoutItem::setGeometry(const QRectF& rect) { void SnapGridLayoutItem::setGeometry(const QRectF& rect) {
QQuickLayoutAttached* info = attachedLayoutObject(m_item, false); QQuickLayoutAttached* info = attachedLayoutObject(m_item, false);
QRectF placed = info ? rect.marginsRemoved(info->effectiveQMargins()) engineRect = info ? rect.marginsRemoved(info->effectiveQMargins()) : rect;
: rect; placed = true;
const QQuickWindow* win = m_item->window();
const qreal dpr = win ? win->effectiveDevicePixelRatio() : 1.0;
if (dpr > 0.0) {
const QPointF origin = absoluteOrigin(m_layout);
const qreal left = (origin.x() + placed.x()) * dpr;
const qreal top = (origin.y() + placed.y()) * dpr;
const qreal right = (origin.x() + placed.right()) * dpr;
const qreal bottom = (origin.y() + placed.bottom()) * dpr;
const qint64 nearX = std::llround(left);
const qint64 nearY = std::llround(top);
const qint64 farX = std::llround(right);
const qint64 farY = std::llround(bottom);
placed = QRectF(
qreal(nearX) / dpr - origin.x(),
qreal(nearY) / dpr - origin.y(),
qreal(farX - nearX) / dpr,
qreal(farY - nearY) / dpr);
}
const QSizeF oldSize(m_item->width(), m_item->height());
const QSizeF newSize = placed.size();
m_item->setPosition(placed.topLeft());
if (newSize == oldSize) {
if (QQuickLayout* lay = qobject_cast<QQuickLayout*>(m_item)) {
if (lay->invalidatedArrangement()) lay->rearrange(newSize);
}
} else {
m_item->setSize(newSize);
}
} }
void SnapLayoutPrivate::mirrorChange() { void SnapLayoutPrivate::mirrorChange() {
@@ -119,6 +84,7 @@ SnapLayout::~SnapLayout() {
void SnapLayout::componentComplete() { void SnapLayout::componentComplete() {
QQuickLayout::componentComplete(); QQuickLayout::componentComplete();
applySpacing();
invalidate(); invalidate();
ensureLayoutItemsUpdated(QQuickLayout::ApplySizeHints); ensureLayoutItemsUpdated(QQuickLayout::ApplySizeHints);
@@ -128,8 +94,12 @@ void SnapLayout::componentComplete() {
void SnapLayout::itemChange(ItemChange change, const ItemChangeData& value) { void SnapLayout::itemChange(ItemChange change, const ItemChangeData& value) {
QQuickLayout::itemChange(change, value); QQuickLayout::itemChange(change, value);
if (change == ItemDevicePixelRatioHasChanged || change == ItemSceneChange) if (change == ItemChildRemovedChange && value.item)
d_func()->childSizes.remove(value.item);
if (change == ItemDevicePixelRatioHasChanged || change == ItemSceneChange) {
applySpacing();
invalidate(); invalidate();
}
} }
QSizeF SnapLayout::sizeHint(Qt::SizeHint whichSizeHint) const { QSizeF SnapLayout::sizeHint(Qt::SizeHint whichSizeHint) const {
@@ -169,16 +139,33 @@ Qt::LayoutDirection SnapLayout::effectiveLayoutDirection() const {
qreal SnapLayout::spacing() const { qreal SnapLayout::spacing() const {
Q_D(const SnapLayout); Q_D(const SnapLayout);
return d->engine.spacing(d->orientation, d->styleInfo); if (!std::isnan(d->spacingRequest)) return d->spacingRequest;
return d->styleInfo->spacing(d->orientation);
}
void SnapLayout::applySpacing() {
Q_D(SnapLayout);
if (!isReady()) return;
const QQuickWindow* win = window();
const qreal dpr = win ? win->effectiveDevicePixelRatio() : 1.0;
qreal space = d->spacingRequest;
if (std::isnan(space)) space = d->styleInfo->spacing(d->orientation);
if (dpr > 0.0) space = qreal(std::llround(space * dpr)) / dpr;
d->engine.setSpacing(space, Qt::Horizontal | Qt::Vertical);
} }
void SnapLayout::setSpacing(qreal space) { void SnapLayout::setSpacing(qreal space) {
Q_D(SnapLayout); Q_D(SnapLayout);
#pragma GCC diagnostic push #pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wfloat-equal" #pragma GCC diagnostic ignored "-Wfloat-equal"
if (std::isnan(space) || spacing() == space) return; if (std::isnan(space) || d->spacingRequest == space) return;
#pragma GCC diagnostic pop #pragma GCC diagnostic pop
d->engine.setSpacing(space, Qt::Horizontal | Qt::Vertical); d->spacingRequest = space;
applySpacing();
invalidate(); invalidate();
emit spacingChanged(); emit spacingChanged();
} }
@@ -242,6 +229,14 @@ void SnapLayout::updateLayoutItems() {
Q_D(SnapLayout); Q_D(SnapLayout);
if (!isReady()) return; if (!isReady()) return;
const auto items = childItems();
for (auto entry = d->childSizes.begin(); entry != d->childSizes.end();) {
if (items.contains(entry.key()))
++entry;
else
entry = d->childSizes.erase(entry);
}
d->engine.deleteItems(); d->engine.deleteItems();
insertLayoutItems(); insertLayoutItems();
} }
@@ -286,8 +281,8 @@ void SnapLayout::insertLayoutItems() {
int gridRow = 0; int gridRow = 0;
int gridColumn = index; int gridColumn = index;
if (d->orientation == Qt::Vertical) qSwap(gridRow, gridColumn); if (d->orientation == Qt::Vertical) qSwap(gridRow, gridColumn);
QGridLayoutItem* layoutItem = new SnapGridLayoutItem( QGridLayoutItem* layoutItem =
child, gridRow, gridColumn, 1, 1, alignment, this); new SnapGridLayoutItem(child, gridRow, gridColumn, 1, 1, alignment);
if (hStretch >= 0) { if (hStretch >= 0) {
if (!fillWidth) if (!fillWidth)
@@ -315,6 +310,7 @@ void SnapLayout::removeGridItem(QGridLayoutItem* gridItem) {
void SnapLayout::itemDestroyed(QQuickItem* item) { void SnapLayout::itemDestroyed(QQuickItem* item) {
if (!isReady()) return; if (!isReady()) return;
Q_D(SnapLayout); Q_D(SnapLayout);
d->childSizes.remove(item);
if (QQuickGridLayoutItem* gridItem = d->engine.findLayoutItem(item)) { if (QQuickGridLayoutItem* gridItem = d->engine.findLayoutItem(item)) {
removeGridItem(gridItem); removeGridItem(gridItem);
delete gridItem; delete gridItem;
@@ -381,6 +377,98 @@ void SnapLayout::clearAncestorListeners() {
d->ancestorListeners.clear(); d->ancestorListeners.clear();
} }
void SnapLayout::arrangeChildren() {
Q_D(SnapLayout);
const QQuickWindow* win = window();
const qreal dpr = win ? win->effectiveDevicePixelRatio() : 1.0;
if (dpr <= 0.0) return;
const bool horiz = d->orientation == Qt::Horizontal;
const QPointF origin = absoluteOrigin(this);
const qint64 originXDev = std::llround(origin.x() * dpr);
const qint64 originYDev = std::llround(origin.y() * dpr);
qreal prevEndExact = 0.0;
qint64 prevNearDev = 0;
qint64 prevSizeDev = 0;
bool started = false;
for (int i = 0; i < d->engine.itemCount(); ++i) {
SnapGridLayoutItem* gridItem =
static_cast<SnapGridLayoutItem*>(d->engine.itemAt(i));
if (!gridItem->placed) continue;
QQuickItem* child = gridItem->layoutItem();
const QRectF& cell = gridItem->engineRect;
const qreal mainExact = (horiz ? cell.x() : cell.y()) * dpr;
const qreal mainSizeExact =
(horiz ? cell.width() : cell.height()) * dpr;
const qreal crossExact = (horiz ? cell.y() : cell.x()) * dpr;
const qint64 mainSizeDev = std::llround(mainSizeExact);
const qint64 crossSizeDev =
std::llround((horiz ? cell.height() : cell.width()) * dpr);
const qint64 engineWidth = horiz ? mainSizeDev : crossSizeDev;
const qint64 engineHeight = horiz ? crossSizeDev : mainSizeDev;
qint64 mainNear = 0;
if (!started) {
mainNear = std::llround(mainExact);
started = true;
} else {
mainNear = prevNearDev + prevSizeDev +
std::llround(mainExact - prevEndExact);
}
const qint64 mainFarDev = std::llround(mainExact + mainSizeExact);
const auto stored = d->childSizes.constFind(child);
qint64 width = engineWidth;
qint64 height = engineHeight;
if (horiz)
width = std::max(mainFarDev - mainNear, qint64(0));
else
height = std::max(mainFarDev - mainNear, qint64(0));
if (stored != d->childSizes.constEnd() &&
stored.value().engineWidth == engineWidth &&
stored.value().engineHeight == engineHeight &&
std::llround(child->width() * dpr) == stored.value().width &&
std::llround(child->height() * dpr) == stored.value().height) {
width = stored.value().width;
height = stored.value().height;
} else {
d->childSizes[child] = SnapLayoutPrivate::ChildSize{
engineWidth, engineHeight, width, height};
}
prevEndExact = mainExact + mainSizeExact;
prevNearDev = mainNear;
prevSizeDev = horiz ? width : height;
const qint64 crossNear = std::llround(crossExact);
const qint64 nearX = originXDev + (horiz ? mainNear : crossNear);
const qint64 nearY = originYDev + (horiz ? crossNear : mainNear);
const QRectF snapped(
qreal(nearX) / dpr - origin.x(),
qreal(nearY) / dpr - origin.y(),
qreal(width) / dpr,
qreal(height) / dpr);
const QSizeF oldSize(child->width(), child->height());
const QSizeF newSize = snapped.size();
child->setPosition(snapped.topLeft());
if (newSize == oldSize) {
if (QQuickLayout* lay = qobject_cast<QQuickLayout*>(child)) {
if (lay->invalidatedArrangement()) lay->rearrange(newSize);
}
} else {
child->setSize(newSize);
}
}
}
void SnapLayout::rearrange(const QSizeF& size) { void SnapLayout::rearrange(const QSizeF& size) {
Q_D(SnapLayout); Q_D(SnapLayout);
if (!isReady() || !size.isValid()) return; if (!isReady() || !size.isValid()) return;
@@ -408,7 +496,11 @@ void SnapLayout::rearrange(const QSizeF& size) {
} }
QQuickLayout::rearrange(size); QQuickLayout::rearrange(size);
for (int i = 0; i < d->engine.itemCount(); ++i)
static_cast<SnapGridLayoutItem*>(d->engine.itemAt(i))->placed = false;
d->engine.setGeometries(QRectF(QPointF(0, 0), size), d->styleInfo); d->engine.setGeometries(QRectF(QPointF(0, 0), size), d->styleInfo);
arrangeChildren();
d->rearranging = false; d->rearranging = false;
for (auto childItem : std::as_const(d->invalidateAfterRearrange)) for (auto childItem : std::as_const(d->invalidateAfterRearrange))
+33 -5
View File
@@ -1,6 +1,8 @@
#pragma once #pragma once
#include <QHash>
#include <QPointer> #include <QPointer>
#include <QtMath>
#include <QtGui/private/qabstractlayoutstyleinfo_p.h> #include <QtGui/private/qabstractlayoutstyleinfo_p.h>
#include <QtGui/private/qgridlayoutengine_p.h> #include <QtGui/private/qgridlayoutengine_p.h>
#include <QtQuickLayouts/private/qquickgridlayoutengine_p.h> #include <QtQuickLayouts/private/qquickgridlayoutengine_p.h>
@@ -20,6 +22,20 @@ class SnapLayoutStyleInfo : public QAbstractLayoutStyleInfo {
bool hasChangedCore() const override; bool hasChangedCore() const override;
}; };
class SnapGridLayoutEngine : public QGridLayoutEngine {
public:
SnapGridLayoutEngine() : QGridLayoutEngine(Qt::AlignVCenter, false) {}
QQuickGridLayoutItem* findLayoutItem(QQuickItem* item) const {
for (int i = int(q_items.size()) - 1; i >= 0; --i) {
QQuickGridLayoutItem* layoutItem =
static_cast<QQuickGridLayoutItem*>(q_items.at(i));
if (layoutItem->layoutItem() == item) return layoutItem;
}
return nullptr;
}
};
class SnapGridLayoutItem : public QQuickGridLayoutItem { class SnapGridLayoutItem : public QQuickGridLayoutItem {
public: public:
SnapGridLayoutItem( SnapGridLayoutItem(
@@ -28,13 +44,12 @@ class SnapGridLayoutItem : public QQuickGridLayoutItem {
int column, int column,
int rowSpan, int rowSpan,
int columnSpan, int columnSpan,
Qt::Alignment alignment, Qt::Alignment alignment);
SnapLayout* layout);
void setGeometry(const QRectF& rect) override; void setGeometry(const QRectF& rect) override;
private: QRectF engineRect;
SnapLayout* m_layout; bool placed = false;
}; };
class SnapLayout : public QQuickLayout { class SnapLayout : public QQuickLayout {
@@ -101,6 +116,10 @@ class SnapLayout : public QQuickLayout {
void insertLayoutItems(); void insertLayoutItems();
void removeGridItem(QGridLayoutItem* gridItem); void removeGridItem(QGridLayoutItem* gridItem);
void arrangeChildren();
void applySpacing();
void updateAncestorListeners(); void updateAncestorListeners();
void clearAncestorListeners(); void clearAncestorListeners();
}; };
@@ -113,10 +132,11 @@ class SnapLayoutPrivate : public QQuickLayoutPrivate {
void mirrorChange() override; void mirrorChange() override;
QQuickGridLayoutEngine engine; SnapGridLayoutEngine engine;
SnapLayoutStyleInfo* styleInfo = nullptr; SnapLayoutStyleInfo* styleInfo = nullptr;
Qt::Orientation orientation = Qt::Horizontal; Qt::Orientation orientation = Qt::Horizontal;
Qt::LayoutDirection layoutDirection = Qt::LeftToRight; Qt::LayoutDirection layoutDirection = Qt::LeftToRight;
qreal spacingRequest = qQNaN();
bool rearranging = false; bool rearranging = false;
int recurRearrangeCounter = 0; int recurRearrangeCounter = 0;
QList<QQuickItem*> invalidateAfterRearrange; QList<QQuickItem*> invalidateAfterRearrange;
@@ -124,6 +144,14 @@ class SnapLayoutPrivate : public QQuickLayoutPrivate {
QVector<QPointer<QQuickItem>> ancestorListeners; QVector<QPointer<QQuickItem>> ancestorListeners;
qint64 originResidualX = -1; qint64 originResidualX = -1;
qint64 originResidualY = -1; qint64 originResidualY = -1;
struct ChildSize {
qint64 engineWidth = 0;
qint64 engineHeight = 0;
qint64 width = 0;
qint64 height = 0;
};
QHash<QQuickItem*, ChildSize> childSizes;
}; };
} // namespace ZShell::components } // namespace ZShell::components
+6 -1
View File
@@ -1,7 +1,10 @@
#pragma once #pragma once
#include "common.hpp"
#include <qqmlintegration.h> #include <qqmlintegration.h>
#include "common.hpp"
#include "enums.hpp"
namespace ZShell::config { namespace ZShell::config {
class Background : public settings::ObjectNode { class Background : public settings::ObjectNode {
@@ -9,6 +12,8 @@ class Background : public settings::ObjectNode {
CONFIG_PROPERTY(bool, enabled, true) CONFIG_PROPERTY(bool, enabled, true)
CONFIG_PROPERTY(int, wallFadeDuration, 300) CONFIG_PROPERTY(int, wallFadeDuration, 300)
CONFIG_ENUM_PROPERTY(
WallpaperTransition, wallTransition, WallpaperTransition::Fade)
}; };
} // namespace ZShell::config } // namespace ZShell::config
+13
View File
@@ -23,6 +23,19 @@ ENUM(NotifsFullscreen, On, Off)
ENUM(TemperatureUnit, Auto, Celsius, Fahrenheit, Kelvin) ENUM(TemperatureUnit, Auto, Celsius, Fahrenheit, Kelvin)
ENUM(DataUnit, Binary, Decimal) ENUM(DataUnit, Binary, Decimal)
ENUM(ClockFormat, Auto, TwelveHour, TwentyFourHour) ENUM(ClockFormat, Auto, TwelveHour, TwentyFourHour)
ENUM(
WallpaperTransition,
Bowtie,
Fade,
LinearBlur,
WindowSlices,
WipeRight,
WipeDown,
TilesWave,
SplitSlideInHorizontal,
Hexagonalize,
DoomMelt,
Random)
#undef ENUM #undef ENUM
+8 -13
View File
@@ -1,8 +1,3 @@
wl_proto(wlp-layer-shell wlr-layer-shell-unstable-v1 "${CMAKE_CURRENT_SOURCE_DIR}/protocols")
wl_proto(wlp-viewporter viewporter "${WAYLAND_PROTOCOLS}/stable/viewporter")
wl_proto(wlp-fractional-scale fractional-scale-v1 "${WAYLAND_PROTOCOLS}/staging/fractional-scale")
wl_proto(wlp-xdg-shell xdg-shell "${WAYLAND_PROTOCOLS}/stable/xdg-shell")
qml_module(ZShell-internal qml_module(ZShell-internal
URI ZShell.Internal URI ZShell.Internal
INCLUDE_PREFIX internal INCLUDE_PREFIX internal
@@ -13,10 +8,6 @@ qml_module(ZShell-internal
circularindicatormanager.hpp circularindicatormanager.cpp circularindicatormanager.hpp circularindicatormanager.cpp
circularbuffer.hpp circularbuffer.cpp circularbuffer.hpp circularbuffer.cpp
arcgauge.hpp arcgauge.cpp arcgauge.hpp arcgauge.cpp
wallpaperimage.hpp wallpaperimage.cpp
wallpaperservice.hpp wallpaperservice.cpp
wallpapersurface.hpp wallpapersurface.cpp
wallpaperobject.hpp wallpaperobject.cpp
lidwatcher.hpp lidwatcher.cpp lidwatcher.hpp lidwatcher.cpp
visualizerbars.hpp visualizerbars.cpp visualizerbars.hpp visualizerbars.cpp
linearindicatormanager.hpp linearindicatormanager.cpp linearindicatormanager.hpp linearindicatormanager.cpp
@@ -27,16 +18,20 @@ qml_module(ZShell-internal
Qt::Gui Qt::Gui
Qt::Quick Qt::Quick
Qt::Concurrent Qt::Concurrent
Qt::Sql
Qt::Core Qt::Core
Qt::Network Qt::Network
Qt::DBus Qt::DBus
Qt::CanvasPainter Qt::CanvasPainter
Qt::GuiPrivate Qt::GuiPrivate
wlp-layer-shell
wlp-xdg-shell
wlp-viewporter
wlp-fractional-scale
PkgConfig::EGL PkgConfig::EGL
PkgConfig::WAYLAND_EGL PkgConfig::WAYLAND_EGL
PkgConfig::GL PkgConfig::GL
ZShell-config
)
qt_add_custom_brush_shaders(ZShell-internal "internal_brush_shaders"
PREFIX "/"
FILES
shaders/strokeholemask.frag
) )
@@ -0,0 +1,22 @@
#version 440
QC_INCLUDE "customfrag.glsl"
float sdRoundRect(vec2 p, vec2 b, float r) {
vec2 q = abs(p) - b + vec2(r);
return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r;
}
void main() {
float keep = 1.0;
if (data4.x > 0.5) {
vec2 p = fragCoord - data1.xy;
float feather = max(data3.y, 0.0001);
keep = smoothstep(-feather, feather, sdRoundRect(p, data1.zw, data3.x));
}
float a = keep * data2.w * globalAlpha * antialiasingAlpha();
fragColor = vec4(data2.rgb * a, a);
}
+39 -12
View File
@@ -71,11 +71,23 @@ static QCanvasPath buildStrokePath(
void StrokeCanvasItem::undoLastGroup() { void StrokeCanvasItem::undoLastGroup() {
if (m_strokes.isEmpty()) return; if (m_strokes.isEmpty()) return;
const int lastGroup = m_strokes.last().groupId; const int lastGroup = m_strokes.last().groupId;
QVector<Stroke> group;
for (int i = static_cast<int>(m_strokes.size()) - 1; for (int i = static_cast<int>(m_strokes.size()) - 1;
i >= 0 && m_strokes[i].groupId == lastGroup; i >= 0 && m_strokes[i].groupId == lastGroup;
--i) --i)
m_strokes.remove(i); group.prepend(m_strokes.takeAt(i));
m_redoStack.append(group);
update();
}
void StrokeCanvasItem::redo() {
if (m_redoStack.isEmpty()) return;
m_strokes.append(m_redoStack.takeLast());
update(); update();
} }
@@ -106,19 +118,14 @@ void StrokeCanvasItem::setHoverPoint(const QPointF& point) {
emit hoverPointChanged(); emit hoverPointChanged();
} }
void StrokeCanvasItem::showHover(qreal x, qreal y) { void StrokeCanvasItem::showHover() {
const QPointF newPoint{x, y}; if (m_hoverVisible) return;
const bool pointChanged = (m_hoverPoint != newPoint);
const bool visibleChanged = !m_hoverVisible;
if (pointChanged || visibleChanged) { m_hoverVisible = true;
m_hoverVisible = true; update();
m_hoverPoint = newPoint;
if (pointChanged) emit hoverPointChanged(); emit hoverVisibleChanged();
if (visibleChanged) emit hoverVisibleChanged();
update();
}
} }
void StrokeCanvasItem::hideHover() { void StrokeCanvasItem::hideHover() {
@@ -139,6 +146,24 @@ void StrokeCanvasItem::setPenWidth(qreal width) {
emit penWidthChanged(); emit penWidthChanged();
} }
void StrokeCanvasItem::setHole(const QRectF& hole) {
if (m_hole == hole) return;
m_hole = hole;
update();
emit holeChanged();
}
void StrokeCanvasItem::setHoleRadius(qreal radius) {
if (qFuzzyCompare(m_holeRadius, radius)) return;
m_holeRadius = radius;
update();
emit holeRadiusChanged();
}
void StrokeCanvasItem::beginStroke(qreal x, qreal y, bool polyline) { void StrokeCanvasItem::beginStroke(qreal x, qreal y, bool polyline) {
m_isDrawing = true; m_isDrawing = true;
m_currentStroke.points.clear(); m_currentStroke.points.clear();
@@ -199,6 +224,7 @@ void StrokeCanvasItem::endStroke() {
m_currentStroke.points.clear(); m_currentStroke.points.clear();
m_strokes.append(m_currentStroke); m_strokes.append(m_currentStroke);
m_currentStroke = {}; m_currentStroke = {};
m_redoStack.clear();
update(); update();
} }
@@ -206,6 +232,7 @@ void StrokeCanvasItem::endStroke() {
void StrokeCanvasItem::clear() { void StrokeCanvasItem::clear() {
m_strokes.clear(); m_strokes.clear();
m_currentStroke = {}; m_currentStroke = {};
m_redoStack.clear();
update(); update();
} }
+16 -1
View File
@@ -3,6 +3,7 @@
#include <QCanvasPainterItem> #include <QCanvasPainterItem>
#include <QColor> #include <QColor>
#include <QPointF> #include <QPointF>
#include <QRectF>
#include <QVector> #include <QVector>
#include <qcolor.h> #include <qcolor.h>
#include <qcontainerfwd.h> #include <qcontainerfwd.h>
@@ -27,6 +28,10 @@ class StrokeCanvasItem : public QCanvasPainterItem {
hoverPointChanged) hoverPointChanged)
Q_PROPERTY( Q_PROPERTY(
qreal penWidth READ penWidth WRITE setPenWidth NOTIFY penWidthChanged) qreal penWidth READ penWidth WRITE setPenWidth NOTIFY penWidthChanged)
Q_PROPERTY(QRectF hole READ hole WRITE setHole NOTIFY holeChanged)
Q_PROPERTY(
qreal holeRadius READ holeRadius WRITE setHoleRadius NOTIFY
holeRadiusChanged)
public: public:
explicit StrokeCanvasItem(QQuickItem* parent = nullptr); explicit StrokeCanvasItem(QQuickItem* parent = nullptr);
@@ -37,15 +42,20 @@ class StrokeCanvasItem : public QCanvasPainterItem {
void setHoverVisible(bool visible); void setHoverVisible(bool visible);
void setHoverPoint(const QPointF& point); void setHoverPoint(const QPointF& point);
Q_INVOKABLE void showHover(qreal x, qreal y); Q_INVOKABLE void showHover();
Q_INVOKABLE void hideHover(); Q_INVOKABLE void hideHover();
Q_INVOKABLE void undoLastGroup(); Q_INVOKABLE void undoLastGroup();
Q_INVOKABLE void redo();
[[nodiscard]] QColor penColor() const { return m_penColor; } [[nodiscard]] QColor penColor() const { return m_penColor; }
[[nodiscard]] qreal penWidth() const { return m_penWidth; } [[nodiscard]] qreal penWidth() const { return m_penWidth; }
[[nodiscard]] QRectF hole() const { return m_hole; }
[[nodiscard]] qreal holeRadius() const { return m_holeRadius; }
void setPenColor(const QColor& color); void setPenColor(const QColor& color);
void setPenWidth(qreal width); void setPenWidth(qreal width);
void setHole(const QRectF& hole);
void setHoleRadius(qreal radius);
Q_INVOKABLE void clear(); Q_INVOKABLE void clear();
@@ -56,6 +66,8 @@ class StrokeCanvasItem : public QCanvasPainterItem {
signals: signals:
void penColorChanged(); void penColorChanged();
void penWidthChanged(); void penWidthChanged();
void holeChanged();
void holeRadiusChanged();
void hoverVisibleChanged(); void hoverVisibleChanged();
void hoverPointChanged(); void hoverPointChanged();
@@ -72,9 +84,12 @@ class StrokeCanvasItem : public QCanvasPainterItem {
qreal m_penWidth = 4.0; qreal m_penWidth = 4.0;
bool m_isDrawing = false; bool m_isDrawing = false;
QRectF m_hole;
qreal m_holeRadius = 0.0;
int m_nextGroupId = 0; int m_nextGroupId = 0;
QVector<Stroke> m_strokes; QVector<Stroke> m_strokes;
Stroke m_currentStroke; Stroke m_currentStroke;
QVector<QVector<Stroke>> m_redoStack;
}; };
}; // namespace ZShell::internal }; // namespace ZShell::internal
+104 -11
View File
@@ -1,11 +1,98 @@
#include "strokecanvasrenderer.hpp" #include "strokecanvasrenderer.hpp"
#include "strokecanvasitem.hpp" #include "strokecanvasitem.hpp"
#include <QCanvasPainter>
#include <QQuickWindow>
#include <QVector4D>
#include <qcolor.h> #include <qcolor.h>
#include <QSet> #include <QSet>
namespace ZShell::internal { namespace ZShell::internal {
static void drawStroke( using Qt::StringLiterals::operator""_s;
static const QString kStrokeHoleShader = u":/shaders/strokeholemask.frag.qsb"_s;
namespace {
struct HoleUniforms {
QVector4D data1; // center.xy, halfSize.xy
QVector4D data3; // x: radius, y: feather in logical px
QVector4D data4; // x: hole enabled
};
static HoleUniforms holeUniforms(
const QRectF& hole, qreal radius, qreal devicePixelRatio) {
const bool active = hole.isValid() && hole.width() > 0 && hole.height() > 0;
if (!active) return {QVector4D(), QVector4D(), QVector4D()};
const QPointF center = hole.center();
const QPointF half(hole.width() * 0.5, hole.height() * 0.5);
const qreal maxRadius = qMin(half.x(), half.y());
return {
QVector4D(
static_cast<float>(center.x()),
static_cast<float>(center.y()),
static_cast<float>(half.x()),
static_cast<float>(half.y())),
QVector4D(
static_cast<float>(qMin(radius, maxRadius)),
static_cast<float>(1.0 / devicePixelRatio),
0.0f,
0.0f),
QVector4D(1.0, 0.0, 0.0, 0.0),
};
}
static void applyHole(QCanvasCustomBrush& brush, const HoleUniforms& u) {
brush.setData1(u.data1);
brush.setData3(u.data3);
brush.setData4(u.data4);
}
}; // namespace
void StrokeCanvasRenderer::updateBrushHole() {
const HoleUniforms uniforms =
holeUniforms(m_hole, m_holeRadius, m_devicePixelRatio);
if (uniforms.data1 == m_lastHoleData1 &&
uniforms.data3 == m_lastHoleData3 && uniforms.data4 == m_lastHoleData4)
return;
m_lastHoleData1 = uniforms.data1;
m_lastHoleData3 = uniforms.data3;
m_lastHoleData4 = uniforms.data4;
++m_holeToken;
for (auto& entry : m_brushes) {
if (entry.holeToken == m_holeToken) continue;
applyHole(entry.brush, uniforms);
entry.holeToken = m_holeToken;
}
}
QCanvasCustomBrush& StrokeCanvasRenderer::brushFor(const QColor& color) {
const QRgb key = color.rgba();
if (auto it = m_brushes.find(key); it != m_brushes.end()) return it->brush;
const HoleUniforms uniforms =
holeUniforms(m_hole, m_holeRadius, m_devicePixelRatio);
BrushEntry entry;
entry.brush.setFragmentShader(kStrokeHoleShader);
applyHole(entry.brush, uniforms);
entry.brush.setData2(
QVector4D(color.redF(), color.greenF(), color.blueF(), color.alphaF()));
entry.holeToken = m_holeToken;
return m_brushes.insert(key, entry)->brush;
}
void StrokeCanvasRenderer::drawStroke(
QCanvasPainter* painter, QCanvasPainter* painter,
const QVector<QPointF>& points, const QVector<QPointF>& points,
const QColor& color, const QColor& color,
@@ -13,8 +100,10 @@ static void drawStroke(
bool polyline) { bool polyline) {
if (points.isEmpty()) return; if (points.isEmpty()) return;
painter->setStrokeStyle(color); QCanvasCustomBrush& brush = brushFor(color);
painter->setFillStyle(color);
painter->setStrokeStyle(brush);
painter->setFillStyle(brush);
painter->setLineWidth(width); painter->setLineWidth(width);
painter->setLineCap(QCanvasPainter::LineCap::Round); painter->setLineCap(QCanvasPainter::LineCap::Round);
painter->setLineJoin(QCanvasPainter::LineJoin::Round); painter->setLineJoin(QCanvasPainter::LineJoin::Round);
@@ -65,7 +154,7 @@ static void drawStroke(
painter->stroke(); painter->stroke();
} }
static void drawHoverCursor( void StrokeCanvasRenderer::drawHoverCursor(
QCanvasPainter* painter, QCanvasPainter* painter,
const QPointF& point, const QPointF& point,
qreal penWidth, qreal penWidth,
@@ -74,7 +163,7 @@ static void drawHoverCursor(
const qreal radius = penWidth * 0.5; const qreal radius = penWidth * 0.5;
if (isDrawing) { if (isDrawing) {
painter->setFillStyle(penColor); painter->setFillStyle(brushFor(penColor));
painter->beginPath(); painter->beginPath();
painter->circle(point, radius); painter->circle(point, radius);
painter->fill(); painter->fill();
@@ -85,7 +174,7 @@ static void drawHoverCursor(
const bool useDashes = penWidth > 10.0; const bool useDashes = penWidth > 10.0;
auto drawOutline = [&](const QColor& color, qreal width) { auto drawOutline = [&](const QColor& color, qreal width) {
painter->setStrokeStyle(color); painter->setStrokeStyle(brushFor(color));
painter->setLineWidth(width); painter->setLineWidth(width);
painter->setLineCap(QCanvasPainter::LineCap::Round); painter->setLineCap(QCanvasPainter::LineCap::Round);
@@ -117,7 +206,7 @@ static void drawHoverCursor(
}; };
auto drawCrosshair = [&](const QColor& color, qreal width) { auto drawCrosshair = [&](const QColor& color, qreal width) {
painter->setStrokeStyle(color); painter->setStrokeStyle(brushFor(color));
painter->setLineWidth(width); painter->setLineWidth(width);
painter->setLineCap(QCanvasPainter::LineCap::Round); painter->setLineCap(QCanvasPainter::LineCap::Round);
@@ -148,8 +237,11 @@ void StrokeCanvasRenderer::synchronizeData(QCanvasPainterItem* item) {
m_penColor = canvas->m_penColor; m_penColor = canvas->m_penColor;
m_penWidth = canvas->m_penWidth; m_penWidth = canvas->m_penWidth;
m_hole = canvas->m_hole;
m_holeRadius = canvas->m_holeRadius;
m_devicePixelRatio =
canvas->window() ? canvas->window()->effectiveDevicePixelRatio() : 1.0;
// Detect removed strokes: present in renderer but no longer in canvas
QSet<int> canvasGroupIds; QSet<int> canvasGroupIds;
for (const auto& stroke : canvas->m_strokes) for (const auto& stroke : canvas->m_strokes)
canvasGroupIds.insert(stroke.groupId); canvasGroupIds.insert(stroke.groupId);
@@ -162,7 +254,6 @@ void StrokeCanvasRenderer::synchronizeData(QCanvasPainterItem* item) {
} }
} }
// Detect added strokes: present in canvas but not yet in renderer
QSet<int> rendererGroupIds; QSet<int> rendererGroupIds;
for (const auto& stroke : m_strokes) for (const auto& stroke : m_strokes)
rendererGroupIds.insert(stroke.groupId); rendererGroupIds.insert(stroke.groupId);
@@ -180,6 +271,8 @@ void StrokeCanvasRenderer::synchronizeData(QCanvasPainterItem* item) {
} }
void StrokeCanvasRenderer::paint(QCanvasPainter* painter) { void StrokeCanvasRenderer::paint(QCanvasPainter* painter) {
updateBrushHole();
for (int id : m_pendingGroupRemovals) for (int id : m_pendingGroupRemovals)
painter->removePathGroup(id); painter->removePathGroup(id);
m_pendingGroupRemovals.clear(); m_pendingGroupRemovals.clear();
@@ -187,8 +280,8 @@ void StrokeCanvasRenderer::paint(QCanvasPainter* painter) {
painter->clearRect(0, 0, width(), height()); painter->clearRect(0, 0, width(), height());
for (const auto& stroke : m_strokes) { for (const auto& stroke : m_strokes) {
painter->setStrokeStyle(stroke.color); painter->setStrokeStyle(brushFor(stroke.color));
painter->setFillStyle(stroke.color); painter->setFillStyle(brushFor(stroke.color));
painter->setLineWidth(stroke.width); painter->setLineWidth(stroke.width);
painter->setLineCap(QCanvasPainter::LineCap::Round); painter->setLineCap(QCanvasPainter::LineCap::Round);
painter->setLineJoin(QCanvasPainter::LineJoin::Round); painter->setLineJoin(QCanvasPainter::LineJoin::Round);
@@ -1,26 +1,58 @@
#pragma once #pragma once
#include <QCanvasCustomBrush>
#include <QCanvasPainterItemRenderer> #include <QCanvasPainterItemRenderer>
#include <QHash>
#include <QRectF>
#include <QVector4D>
#include <qcontainerfwd.h> #include <qcontainerfwd.h>
#include "stroke.hpp" #include "stroke.hpp"
namespace ZShell::internal { namespace ZShell::internal {
struct BrushEntry {
QCanvasCustomBrush brush;
qint64 holeToken = -1; // hole generation this brush was built with
};
class StrokeCanvasRenderer final : public QCanvasPainterItemRenderer { class StrokeCanvasRenderer final : public QCanvasPainterItemRenderer {
public: public:
void synchronizeData(QCanvasPainterItem* item) override; void synchronizeData(QCanvasPainterItem* item) override;
void paint(QCanvasPainter* painter) override; void paint(QCanvasPainter* painter) override;
private: private:
QCanvasCustomBrush& brushFor(const QColor& color);
void updateBrushHole();
void drawStroke(
QCanvasPainter* painter,
const QVector<QPointF>& points,
const QColor& color,
qreal width,
bool polyline);
void drawHoverCursor(
QCanvasPainter* painter,
const QPointF& point,
qreal penWidth,
QColor penColor,
bool isDrawing);
QColor m_penColor; QColor m_penColor;
qreal m_penWidth = 4.0; qreal m_penWidth = 4.0;
bool m_hoverVisible = false; bool m_hoverVisible = false;
QPointF m_hoverPoint; QPointF m_hoverPoint;
bool m_isDrawing = false; bool m_isDrawing = false;
QRectF m_hole;
qreal m_holeRadius = 0.0;
qreal m_devicePixelRatio = 1.0;
QVector4D m_lastHoleData1;
QVector4D m_lastHoleData3;
QVector4D m_lastHoleData4;
qint64 m_holeToken = 0;
QVector<Stroke> m_strokes; QVector<Stroke> m_strokes;
Stroke m_currentStroke; Stroke m_currentStroke;
QVector<int> m_pendingGroupRemovals; QVector<int> m_pendingGroupRemovals;
QHash<QRgb, BrushEntry> m_brushes;
}; };
}; // namespace ZShell::internal }; // namespace ZShell::internal
@@ -1,756 +0,0 @@
#define EGL_EGLEXT_PROTOTYPES
#define GL_GLEXT_PROTOTYPES
#include "wallpaperservice.hpp"
#include <EGL/eglext.h>
#include <GL/gl.h>
#include <GL/glext.h>
#include <fcntl.h>
#include <poll.h>
#include <unistd.h>
#include <QDebug>
#include <QGuiApplication>
#include <QImageReader>
#include <QScreen>
#include <qguiapplication_platform.h>
#include <private/qwaylandscreen_p.h>
#include <cstring>
namespace ZShell::internal {
namespace {
const char* kVertexShader = R"(
#version 300 es
void main() {
// The corners are (-1,-1), (3,-1) and (-1,3), which cover the whole buffer.
vec2 corner = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
gl_Position = vec4(corner * 2.0 - 1.0, 0.0, 1.0);
}
)";
const char* kFragmentShader = R"(
#version 300 es
precision highp float;
uniform sampler2D source;
uniform vec2 size;
uniform vec4 crop;
out vec4 outColor;
void main() {
// The pixels are stored top-down, so the fragment coordinate is flipped.
vec2 p = vec2(gl_FragCoord.x, size.y - gl_FragCoord.y) / size;
outColor = texture(source, crop.xy + p * crop.zw);
}
)";
constexpr int kWaitTimeoutMs = 20;
void logShader(uint32_t object, bool program) {
int ok = 0;
if (program)
glGetProgramiv(object, GL_LINK_STATUS, &ok);
else
glGetShaderiv(object, GL_COMPILE_STATUS, &ok);
if (ok) return;
char info[512] = {};
if (program)
glGetProgramInfoLog(object, int(sizeof(info)), nullptr, info);
else
glGetShaderInfoLog(object, int(sizeof(info)), nullptr, info);
qWarning() << "WallpaperService:" << (program ? "linking" : "compiling")
<< "failed:" << info;
}
QVariantMap cropFor(const QVariantMap& crops, const QString& screen) {
QVariantMap crop = crops.value(screen).toMap();
if (crop.isEmpty()) {
crop.insert("x", 0.0);
crop.insert("y", 0.0);
crop.insert("width", 1.0);
crop.insert("height", 1.0);
crop.insert("zoom", 1.0);
}
return crop;
}
} // namespace
WallpaperService::WallpaperService(QObject* parent) : QObject(parent) {}
WallpaperService::~WallpaperService() {
m_stop = true;
wake();
if (m_thread.joinable()) m_thread.join();
for (auto* surface : m_surfaces)
delete surface;
m_surfaces.clear();
if (m_eglDisplay != EGL_NO_DISPLAY && m_context != EGL_NO_CONTEXT) {
if (eglMakeCurrent(
m_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, m_context)) {
if (m_texture) glDeleteTextures(1, &m_texture);
if (m_program) glDeleteProgram(m_program);
}
eglDestroyContext(m_eglDisplay, m_context);
}
if (m_queue) wl_event_queue_destroy(m_queue);
if (m_wake[0] >= 0) {
close(m_wake[0]);
close(m_wake[1]);
}
if (m_layerShell) m_layerShell->destroy();
if (m_viewporter) m_viewporter->destroy();
if (m_scaleManager) m_scaleManager->destroy();
if (m_registry) wl_registry_destroy(m_registry);
}
QString WallpaperService::path() const {
std::lock_guard<std::recursive_mutex> lock(m_mutex);
return m_path;
}
void WallpaperService::setPath(const QString& path) {
if (m_path == path) return;
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
m_path = path;
m_forceDecode = true;
m_imageChanged = true;
}
emit pathChanged();
start();
queueAll();
}
void WallpaperService::addScreen(const QString& screen) {
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
if (m_screens.contains(screen)) return;
m_screens.append(screen);
}
start();
queueAll();
}
void WallpaperService::removeScreen(const QString& screen) {
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
if (!m_screens.removeOne(screen)) return;
}
queueAll();
}
const QVariantMap& WallpaperService::crops() const {
std::lock_guard<std::recursive_mutex> lock(m_mutex);
return m_crops;
}
void WallpaperService::setCrops(const QVariantMap& crops) {
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
if (m_crops == crops) return;
m_crops = crops;
}
emit cropsChanged();
queueAll();
}
void WallpaperService::setCrop(const QString& screen, const QVariantMap& crop) {
WallpaperSurface* surface = nullptr;
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
if (m_crops.value(screen) == crop) return;
m_crops.insert(screen, crop);
for (auto* candidate : m_surfaces) {
if (candidate->screen() == screen) {
surface = candidate;
break;
}
}
}
emit cropsChanged();
if (surface) queue(surface);
}
void WallpaperService::redraw() {
queueAll();
}
void WallpaperService::start() {
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
if (started || m_path.isEmpty() || m_screens.isEmpty()) return;
started = true;
}
auto* interface =
qApp->nativeInterface<QNativeInterface::QWaylandApplication>();
if (!interface) {
qWarning() << "WallpaperService: this is not a Wayland session";
return;
}
m_display = interface->display();
m_compositor = interface->compositor();
if (!m_display || !m_compositor) return;
m_queue = wl_display_create_queue(m_display);
if (!m_queue) return;
if (pipe2(m_wake, O_CLOEXEC) < 0) {
qWarning() << "WallpaperService: could not make the pipe to wake on";
return;
}
bindGlobals();
if (!setupGl()) return;
m_drawing = true;
m_thread = std::thread([this] { loop(); });
wake();
}
void WallpaperService::wake() {
if (m_wake[1] < 0) return;
char byte = 0;
if (::write(m_wake[1], &byte, 1) < 0)
qWarning() << "WallpaperService: could not wake the drawing thread";
}
void WallpaperService::queue(WallpaperSurface* surface) {
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
m_pending.push_back(surface);
}
wake();
}
void WallpaperService::queueAll() {
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
for (auto* surface : m_surfaces)
m_pending.push_back(surface);
}
wake();
}
void WallpaperService::loop() {
while (!m_stop) {
present();
if (wl_display_prepare_read(m_display) == 0) {
wl_display_flush(m_display);
struct pollfd fds[2] = {
{wl_display_get_fd(m_display), POLLIN, 0},
{m_wake[0], POLLIN, 0},
};
int ready = poll(fds, 2, kWaitTimeoutMs);
if (ready > 0 && fds[0].revents) {
wl_display_read_events(m_display);
continue;
}
if (ready > 0 && fds[1].revents) {
char byte = 0;
::read(m_wake[0], &byte, 1);
}
wl_display_cancel_read(m_display);
continue;
}
struct pollfd fds[1] = {{m_wake[0], POLLIN, 0}};
if (poll(fds, 1, kWaitTimeoutMs) <= 0) continue;
char byte = 0;
::read(m_wake[0], &byte, 1);
}
}
void WallpaperService::present() {
wl_display_dispatch_queue_pending(m_display, m_queue);
addSurfaces();
ensureDecodeSize();
std::vector<WallpaperSurface*> pending;
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
pending.swap(m_pending);
}
for (auto* surface : pending)
drawSurface(surface);
}
void WallpaperService::addSurfaces() {
if (!started || !m_layerShell || !m_viewporter || !m_scaleManager) return;
QList<WallpaperSurface*> gone;
QList<QString> needed;
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
QList<WallpaperSurface*> kept;
for (auto* surface : m_surfaces) {
if (m_screens.contains(surface->screen()))
kept.append(surface);
else
gone.append(surface);
}
m_surfaces = kept;
for (const auto& screen : m_screens) {
bool exists = false;
for (auto* surface : m_surfaces) {
if (surface->screen() == screen) {
exists = true;
break;
}
}
if (!exists) needed.append(screen);
}
}
for (auto* surface : gone)
delete surface;
for (const auto& screen : needed)
addSurface(screen);
}
void WallpaperService::registryGlobal(
void* data,
struct wl_registry* registry,
uint32_t name,
const char* interface,
uint32_t version) {
auto* service = static_cast<WallpaperService*>(data);
void* proxy = nullptr;
if (strcmp(interface, "zwlr_layer_shell_v1") == 0) {
proxy = new QtWayland::zwlr_layer_shell_v1(
registry, int(name), int(version));
} else if (strcmp(interface, "wp_viewporter") == 0) {
proxy = new QtWayland::wp_viewporter(registry, int(name), int(version));
} else if (strcmp(interface, "wp_fractional_scale_manager_v1") == 0) {
proxy = new QtWayland::wp_fractional_scale_manager_v1(
registry, int(name), int(version));
} else {
return;
}
service->storeGlobal(interface, proxy);
service->queueAll();
}
void WallpaperService::storeGlobal(const char* interface, void* proxy) {
std::lock_guard<std::recursive_mutex> lock(m_mutex);
if (strcmp(interface, "zwlr_layer_shell_v1") == 0)
m_layerShell = static_cast<QtWayland::zwlr_layer_shell_v1*>(proxy);
else if (strcmp(interface, "wp_viewporter") == 0)
m_viewporter = static_cast<QtWayland::wp_viewporter*>(proxy);
else if (strcmp(interface, "wp_fractional_scale_manager_v1") == 0)
m_scaleManager =
static_cast<QtWayland::wp_fractional_scale_manager_v1*>(proxy);
}
void WallpaperService::bindGlobals() {
m_registry = wl_display_get_registry(m_display);
static const struct wl_registry_listener listener = {
registryGlobal,
nullptr,
};
wl_registry_add_listener(m_registry, &listener, this);
}
bool WallpaperService::setupGl() {
const char* extensions = eglQueryString(EGL_NO_DISPLAY, EGL_EXTENSIONS);
if (extensions && strstr(extensions, "EGL_EXT_platform_wayland")) {
m_eglDisplay =
eglGetPlatformDisplay(EGL_PLATFORM_WAYLAND_EXT, m_display, nullptr);
} else {
m_eglDisplay =
eglGetDisplay(reinterpret_cast<EGLNativeDisplayType>(m_display));
}
if (m_eglDisplay == EGL_NO_DISPLAY) {
qWarning() << "WallpaperService: no EGL display";
return false;
}
EGLint major = 0, minor = 0;
if (!eglInitialize(m_eglDisplay, &major, &minor)) {
qWarning() << "WallpaperService: eglInitialize failed";
return false;
}
const EGLint configAttributes[] = {
EGL_SURFACE_TYPE,
EGL_WINDOW_BIT,
EGL_RENDERABLE_TYPE,
EGL_OPENGL_ES2_BIT,
EGL_RED_SIZE,
8,
EGL_GREEN_SIZE,
8,
EGL_BLUE_SIZE,
8,
EGL_ALPHA_SIZE,
8,
EGL_NONE};
EGLint count = 0;
if (!eglChooseConfig(m_eglDisplay, configAttributes, &m_config, 1, &count) ||
count == 0) {
qWarning() << "WallpaperService: no EGL config for window surfaces";
return false;
}
const EGLint contextAttributes[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_NONE};
m_context = eglCreateContext(
m_eglDisplay, m_config, EGL_NO_CONTEXT, contextAttributes);
if (m_context == EGL_NO_CONTEXT) {
qWarning() << "WallpaperService: eglCreateContext failed";
return false;
}
return true;
}
uint32_t WallpaperService::createProgram() {
uint32_t vertex = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex, 1, &kVertexShader, nullptr);
glCompileShader(vertex);
logShader(vertex, false);
uint32_t fragment = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragment, 1, &kFragmentShader, nullptr);
glCompileShader(fragment);
logShader(fragment, false);
uint32_t program = glCreateProgram();
glAttachShader(program, vertex);
glAttachShader(program, fragment);
glLinkProgram(program);
logShader(program, true);
if (glGetUniformLocation(program, "size") < 0 ||
glGetUniformLocation(program, "crop") < 0) {
qWarning() << "WallpaperService: the program does not have the "
"uniforms it needs";
glDeleteProgram(program);
return 0;
}
m_sizeUniform = glGetUniformLocation(program, "size");
m_cropUniform = glGetUniformLocation(program, "crop");
glDeleteShader(vertex);
glDeleteShader(fragment);
return program;
}
void WallpaperService::uploadImage() {
if (m_image.isNull()) return;
if (m_texture == 0) {
glGenTextures(1, &m_texture);
glBindTexture(GL_TEXTURE_2D, m_texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
} else {
glBindTexture(GL_TEXTURE_2D, m_texture);
}
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexImage2D(
GL_TEXTURE_2D,
0,
GL_RGBA8,
m_image.width(),
m_image.height(),
0,
GL_RGBA,
GL_UNSIGNED_BYTE,
m_image.constBits());
}
bool WallpaperService::decode(const QString& path) {
if (path.isEmpty()) return false;
QImageReader reader(path);
reader.setAutoTransform(true);
QSize size = reader.size();
if (size.isValid()) m_sourceSize = size;
if (!m_decodeSize.isEmpty() && !m_sourceSize.isEmpty())
reader.setScaledSize(
m_sourceSize.scaled(m_decodeSize, Qt::KeepAspectRatioByExpanding));
QImage image = reader.read();
if (image.isNull()) {
qWarning() << "WallpaperService: could not read the image" << path;
return false;
}
m_image = image.convertToFormat(QImage::Format_RGBA8888);
return !m_image.isNull();
}
bool WallpaperService::ensureDecodeSize() {
QSize largest;
QString path;
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
largest = largestBufferSize();
if (largest.isEmpty()) return false;
if (!m_forceDecode && !m_decodeSize.isEmpty() &&
qint64(largest.width()) * largest.height() <=
qint64(m_decodeSize.width()) * m_decodeSize.height())
return false;
QSize target =
m_sourceSize.isEmpty()
? largest
: m_sourceSize.scaled(largest, Qt::KeepAspectRatioByExpanding);
if (!m_forceDecode && !m_image.isNull() && target == m_image.size())
return false;
m_forceDecode = false;
m_decodeSize = largest;
path = m_path;
}
if (!decode(path)) return false;
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
m_imageChanged = true;
}
return true;
}
QSize WallpaperService::largestBufferSize() {
QSize largest;
for (auto* surface : m_surfaces) {
QSize buffer = surface->bufferSize();
if (buffer.isEmpty()) continue;
if (largest.isEmpty() || qint64(buffer.width()) * buffer.height() >
qint64(largest.width()) * largest.height())
largest = buffer;
}
return largest;
}
QRectF WallpaperService::sourceRect(
const QVariantMap& crops, const QString& screen, const QSize& size) const {
QVariantMap crop = cropFor(crops, screen);
QRectF rect(
crop.value("x", 0.0).toDouble(),
crop.value("y", 0.0).toDouble(),
crop.value("width", 1.0).toDouble(),
crop.value("height", 1.0).toDouble());
if (size.isEmpty() || rect.isEmpty() || m_image.isNull()) return rect;
qreal imageWidth = qreal(m_image.width());
qreal imageHeight = qreal(m_image.height());
QRectF pixels(
rect.x() * imageWidth,
rect.y() * imageHeight,
rect.width() * imageWidth,
rect.height() * imageHeight);
qreal targetRatio = qreal(size.width()) / qreal(size.height());
qreal rectRatio = pixels.width() / pixels.height();
if (rectRatio > targetRatio) {
qreal newWidth = pixels.height() * targetRatio;
pixels.setX(pixels.x() + (pixels.width() - newWidth) / 2.0);
pixels.setWidth(newWidth);
} else if (rectRatio < targetRatio) {
qreal newHeight = pixels.width() / targetRatio;
pixels.setY(pixels.y() + (pixels.height() - newHeight) / 2.0);
pixels.setHeight(newHeight);
}
return QRectF(
pixels.x() / imageWidth,
pixels.y() / imageHeight,
pixels.width() / imageWidth,
pixels.height() / imageHeight);
}
void WallpaperService::drawSurface(WallpaperSurface* surface) {
EGLSurface target = surface->eglSurface();
if (m_context == EGL_NO_CONTEXT || target == EGL_NO_SURFACE ||
surface->bufferSize().isEmpty())
return;
if (!eglMakeCurrent(m_eglDisplay, target, target, m_context)) {
qWarning() << "WallpaperService: the context cannot be made current";
return;
}
// Release the context when this scope ends (see FrameService::drawSurface).
// Holding it across the draw loop's sleep serializes the two raw-EGL clients on
// NVIDIA, which makes eglSwapBuffers fail and starves other processes' contexts.
struct ContextRelease {
EGLDisplay display;
~ContextRelease() {
eglMakeCurrent(
display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
}
} contextRelease{m_eglDisplay};
QVariantMap crops;
QSize size;
bool upload = false;
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
crops = m_crops;
size = surface->bufferSize();
upload = m_imageChanged;
m_imageChanged = false;
}
if (size.isEmpty()) return;
if (m_program == 0) m_program = createProgram();
if (m_texture == 0 || upload) uploadImage();
if (m_program == 0 || m_texture == 0 || m_image.isNull()) return;
glUseProgram(m_program);
glBindTexture(GL_TEXTURE_2D, m_texture);
glUniform2f(
GLint(m_sizeUniform), GLfloat(size.width()), GLfloat(size.height()));
QRectF rect = sourceRect(crops, surface->screen(), size);
glUniform4f(
GLint(m_cropUniform),
GLfloat(rect.x()),
GLfloat(rect.y()),
GLfloat(rect.width()),
GLfloat(rect.height()));
glViewport(0, 0, size.width(), size.height());
glDrawArrays(GL_TRIANGLES, 0, 3);
if (!eglSwapBuffers(m_eglDisplay, target))
qWarning()
<< "WallpaperService: eglSwapBuffers failed, eglGetError = 0x"
<< QString::number(unsigned(eglGetError()), 16);
}
void WallpaperService::addSurface(const QString& screen) {
if (!m_layerShell || !m_viewporter || !m_scaleManager) return;
QScreen* qscreen = nullptr;
for (auto* candidate : QGuiApplication::screens()) {
if (candidate->name() == screen) {
qscreen = candidate;
break;
}
}
if (!qscreen) {
qWarning() << "WallpaperService:" << screen << "is not a screen";
return;
}
auto* waylandScreen =
dynamic_cast<QtWaylandClient::QWaylandScreen*>(qscreen->handle());
if (!waylandScreen || !waylandScreen->output()) {
qWarning() << "WallpaperService:" << screen
<< "is not a Wayland screen";
return;
}
wl_surface* wlSurface = wl_compositor_create_surface(m_compositor);
if (!wlSurface) return;
auto* surface =
new WallpaperSurface(this, wlSurface, waylandScreen->output(), screen);
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
m_surfaces.append(surface);
}
}
} // namespace ZShell::internal
@@ -1,147 +0,0 @@
#pragma once
#include <EGL/egl.h>
#include <cstdint>
#include <mutex>
#include <thread>
#include <vector>
#include <QImage>
#include <QList>
#include <QRectF>
#include <QSize>
#include <QString>
#include <QStringList>
#include <QVariantMap>
#include <QtQml/qqml.h>
#include <qtmetamacros.h>
#include <qwayland-wlr-layer-shell-unstable-v1.h>
#include <wayland-client-protocol.h>
#include "wallpapersurface.hpp"
namespace ZShell::internal {
class WallpaperSurface;
class WallpaperService : public QObject {
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged REQUIRED)
Q_PROPERTY(QVariantMap crops READ crops WRITE setCrops NOTIFY cropsChanged)
public:
explicit WallpaperService(QObject* parent = nullptr);
~WallpaperService() override;
[[nodiscard]] QString path() const;
void setPath(const QString& path);
void addScreen(const QString& screen);
void removeScreen(const QString& screen);
[[nodiscard]] const QVariantMap& crops() const;
void setCrops(const QVariantMap& crops);
Q_INVOKABLE void redraw();
Q_INVOKABLE void setCrop(const QString& screen, const QVariantMap& crop);
signals:
void pathChanged();
void cropsChanged();
private:
friend class WallpaperSurface;
void bindGlobals();
static void registryGlobal(
void* data,
struct wl_registry* registry,
uint32_t name,
const char* interface,
uint32_t version);
void storeGlobal(const char* interface, void* proxy);
[[nodiscard]] bool setupGl();
[[nodiscard]] uint32_t createProgram();
void uploadImage();
bool decode(const QString& path);
bool ensureDecodeSize();
[[nodiscard]] QSize largestBufferSize();
void queue(WallpaperSurface* surface);
void queueAll();
void startDrawing();
void loop();
void present();
void drawSurface(WallpaperSurface* surface);
void wake();
[[nodiscard]] wl_event_queue* eventQueue() const { return m_queue; }
[[nodiscard]] QRectF sourceRect(
const QVariantMap& crops,
const QString& screen,
const QSize& size) const;
void start();
void addSurfaces();
void addSurface(const QString& screen);
QString m_path;
QStringList m_screens;
QVariantMap m_crops;
QImage m_image;
QSize m_sourceSize;
QSize m_decodeSize;
wl_display* m_display = nullptr;
wl_registry* m_registry = nullptr;
wl_compositor* m_compositor = nullptr;
QtWayland::zwlr_layer_shell_v1* m_layerShell = nullptr;
QtWayland::wp_viewporter* m_viewporter = nullptr;
QtWayland::wp_fractional_scale_manager_v1* m_scaleManager = nullptr;
EGLDisplay m_eglDisplay = EGL_NO_DISPLAY;
EGLConfig m_config = nullptr;
EGLContext m_context = EGL_NO_CONTEXT;
uint32_t m_texture = 0;
uint32_t m_program = 0;
int m_sizeUniform = -1;
int m_cropUniform = -1;
QList<WallpaperSurface*> m_surfaces;
std::thread m_thread;
mutable std::recursive_mutex m_mutex;
std::vector<WallpaperSurface*> m_pending;
wl_event_queue* m_queue = nullptr;
int m_wake[2] = {-1, -1};
bool m_stop = false;
bool m_drawing = false;
bool started = false;
bool m_forceDecode = false;
bool m_imageChanged = false;
};
} // namespace ZShell::internal
+5
View File
@@ -2,7 +2,12 @@ qml_module(ZShell-models
URI ZShell.Models URI ZShell.Models
SOURCES SOURCES
filesystemmodel.hpp filesystemmodel.cpp filesystemmodel.hpp filesystemmodel.cpp
wallpaperdb.hpp wallpaperdb.cpp
wallpaperutil.hpp
LIBRARIES LIBRARIES
Qt::Gui Qt::Gui
Qt::Quick
Qt::Qml
Qt::Concurrent Qt::Concurrent
Qt::Sql
) )
+144 -14
View File
@@ -1,11 +1,84 @@
#include "filesystemmodel.hpp" #include "filesystemmodel.hpp"
#include <qcoreapplication.h>
#include <qdatetime.h>
#include <qdir.h>
#include <qdiriterator.h> #include <qdiriterator.h>
#include <qfile.h>
#include <qfileinfo.h>
#include <qfuturewatcher.h> #include <qfuturewatcher.h>
#include <qimage.h>
#include <qpointer.h>
#include <qprocess.h>
#include <qregularexpression.h>
#include <qtemporarydir.h>
#include <qtconcurrentrun.h> #include <qtconcurrentrun.h>
#include <algorithm>
#include <mutex>
namespace ZShell::models { namespace ZShell::models {
const QStringList kVideoExtensions = {
"mp4",
"mkv",
"webm",
"avi",
"mov",
"wmv",
"flv",
"m4v",
"mpg",
"mpeg",
"3gp",
"ogv",
"ts",
"vob",
};
bool hasVideoExtension(const QString& path) {
return kVideoExtensions.contains(QFileInfo(path).suffix().toLower());
}
const QStringList kHardwareCodecs = {
"av1",
"hevc",
"h264",
"vp9",
"vp8",
"vc1",
"mpeg2",
"mpeg2video",
"mpeg4",
"mjpeg",
};
bool probeHardwareCodec(const QString& path) {
QProcess process;
process.start(
"ffprobe",
{
"-v",
"error",
"-select_streams",
"v:0",
"-show_entries",
"stream=codec_name",
"-of",
"default=nw=1:nk=1",
path,
});
process.waitForFinished(5000);
if (process.exitCode() != 0) {
return false;
}
const QString codec =
QString::fromUtf8(process.readAllStandardOutput()).trimmed().toLower();
return kHardwareCodecs.contains(codec);
}
FileSystemEntry::FileSystemEntry( FileSystemEntry::FileSystemEntry(
const QString& path, const QString& relativePath, QObject* parent) const QString& path, const QString& relativePath, QObject* parent)
: QObject(parent) : QObject(parent)
@@ -56,6 +129,14 @@ bool FileSystemEntry::isImage() const {
return m_isImage; return m_isImage;
} }
bool FileSystemEntry::isVideo() const {
return hasVideoExtension(m_path);
}
QString FileSystemEntry::thumbnailPath() const {
return m_path;
}
QString FileSystemEntry::mimeType() const { QString FileSystemEntry::mimeType() const {
if (!m_mimeTypeInitialised) { if (!m_mimeTypeInitialised) {
const QMimeDatabase db; const QMimeDatabase db;
@@ -221,6 +302,46 @@ void FileSystemModel::setNameFilters(const QStringList& nameFilters) {
update(); update();
} }
bool FileSystemModel::requireHardwareCodec() const {
return m_requireHardwareCodec;
}
void FileSystemModel::setRequireHardwareCodec(bool requireHardwareCodec) {
if (m_requireHardwareCodec == requireHardwareCodec) {
return;
}
m_requireHardwareCodec = requireHardwareCodec;
emit requireHardwareCodecChanged();
update();
}
bool FileSystemModel::isHardwareVideo(const QString& path) {
const QFileInfo info(path);
const QString key =
path + '|' + QByteArray::number(info.size()) + '|' +
QByteArray::number(info.lastModified().toMSecsSinceEpoch() / 1000);
{
std::lock_guard<QMutex> lock(m_codecMutex);
const auto it = m_codecCache.constFind(key);
if (it != m_codecCache.constEnd()) {
return it.value();
}
}
const bool hardware = probeHardwareCodec(path);
{
std::lock_guard<QMutex> lock(m_codecMutex);
m_codecCache.insert(key, hardware);
}
return hardware;
}
QQmlListProperty<FileSystemEntry> FileSystemModel::entries() { QQmlListProperty<FileSystemEntry> FileSystemModel::entries() {
return QQmlListProperty<FileSystemEntry>(this, &m_entries); return QQmlListProperty<FileSystemEntry>(this, &m_entries);
} }
@@ -251,14 +372,12 @@ void FileSystemModel::watchDirIfRecursive(const QString& path) {
const auto paths = watcher->result(); const auto paths = watcher->result();
if (currentDir == m_dir && showHidden == m_showHidden && if (currentDir == m_dir && showHidden == m_showHidden &&
!paths.isEmpty()) { !paths.isEmpty()) {
// Ignore if dir or showHidden has changed
m_watcher.addPaths(paths); m_watcher.addPaths(paths);
} }
watcher->deleteLater(); watcher->deleteLater();
}); });
watcher->setFuture(future); watcher->setFuture(future);
} }
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks)
} }
void FileSystemModel::update() { void FileSystemModel::update() {
@@ -305,6 +424,7 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) {
const auto showHidden = m_showHidden; const auto showHidden = m_showHidden;
const auto filter = m_filter; const auto filter = m_filter;
const auto nameFilters = m_nameFilters; const auto nameFilters = m_nameFilters;
const auto requireHardware = m_requireHardwareCodec;
QSet<QString> oldPaths; QSet<QString> oldPaths;
for (const auto& entry : std::as_const(m_entries)) { for (const auto& entry : std::as_const(m_entries)) {
@@ -312,17 +432,26 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) {
} }
const auto future = QtConcurrent::run( const auto future = QtConcurrent::run(
[=](QPromise<QPair<QSet<QString>, QSet<QString>>>& promise) { [=, this](QPromise<QPair<QSet<QString>, QSet<QString>>>& promise) {
const auto flags = recursive ? QDirIterator::Subdirectories const auto flags = recursive ? QDirIterator::Subdirectories
: QDirIterator::NoIteratorFlags; : QDirIterator::NoIteratorFlags;
std::optional<QDirIterator> iter; std::optional<QDirIterator> iter;
if (filter == Images) { if (filter == Images || filter == Videos || filter == Media) {
QStringList extraNameFilters = nameFilters; QStringList extraNameFilters = nameFilters;
const auto formats = QImageReader::supportedImageFormats();
for (const auto& format : formats) { if (filter == Images || filter == Media) {
extraNameFilters << "*." + format; const auto formats = QImageReader::supportedImageFormats();
for (const auto& format : formats) {
extraNameFilters << "*." + format;
}
}
if (filter == Videos || filter == Media) {
for (const auto& ext : kVideoExtensions) {
extraNameFilters << "*." + ext;
}
} }
QDir::Filters filters = QDir::Files; QDir::Filters filters = QDir::Files;
@@ -361,7 +490,11 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) {
QString path = iter->next(); QString path = iter->next();
if (filter == Images) { if (hasVideoExtension(path)) {
if (requireHardware && !isHardwareVideo(path)) {
continue;
}
} else if (filter == Images || filter == Media) {
QImageReader reader(path); QImageReader reader(path);
if (!reader.canRead()) { if (!reader.canRead()) {
continue; continue;
@@ -406,7 +539,6 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) {
}); });
watcher->setFuture(future); watcher->setFuture(future);
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks)
} }
void FileSystemModel::applyChanges( void FileSystemModel::applyChanges(
@@ -419,7 +551,6 @@ void FileSystemModel::applyChanges(
} }
std::sort(removedIndices.begin(), removedIndices.end(), std::greater<int>()); std::sort(removedIndices.begin(), removedIndices.end(), std::greater<int>());
// Batch remove old entries
int start = -1; int start = -1;
int end = -1; int end = -1;
for (int idx : std::as_const(removedIndices)) { for (int idx : std::as_const(removedIndices)) {
@@ -447,11 +578,11 @@ void FileSystemModel::applyChanges(
endRemoveRows(); endRemoveRows();
} }
// Create new entries
QList<FileSystemEntry*> newEntries; QList<FileSystemEntry*> newEntries;
for (const auto& path : addedPaths) { for (const auto& path : addedPaths) {
newEntries << new FileSystemEntry( auto* entry =
path, m_dir.relativeFilePath(path), this); new FileSystemEntry(path, m_dir.relativeFilePath(path), this);
newEntries << entry;
} }
std::sort( std::sort(
newEntries.begin(), newEntries.begin(),
@@ -460,7 +591,6 @@ void FileSystemModel::applyChanges(
return compareEntries(a, b); return compareEntries(a, b);
}); });
// Batch insert new entries
int insertStart = -1; int insertStart = -1;
QList<FileSystemEntry*> batchItems; QList<FileSystemEntry*> batchItems;
for (const auto& entry : std::as_const(newEntries)) { for (const auto& entry : std::as_const(newEntries)) {
+19 -1
View File
@@ -4,7 +4,9 @@
#include <qdir.h> #include <qdir.h>
#include <qfilesystemwatcher.h> #include <qfilesystemwatcher.h>
#include <qfuture.h> #include <qfuture.h>
#include <qhash.h>
#include <qimagereader.h> #include <qimagereader.h>
#include <qmutex.h>
#include <qmimedatabase.h> #include <qmimedatabase.h>
#include <qobject.h> #include <qobject.h>
#include <qqmlintegration.h> #include <qqmlintegration.h>
@@ -28,6 +30,8 @@ class FileSystemEntry : public QObject {
Q_PROPERTY(qint64 size READ size CONSTANT) Q_PROPERTY(qint64 size READ size CONSTANT)
Q_PROPERTY(bool isDir READ isDir CONSTANT) Q_PROPERTY(bool isDir READ isDir CONSTANT)
Q_PROPERTY(bool isImage READ isImage CONSTANT) Q_PROPERTY(bool isImage READ isImage CONSTANT)
Q_PROPERTY(bool isVideo READ isVideo CONSTANT)
Q_PROPERTY(QString thumbnailPath READ thumbnailPath CONSTANT)
Q_PROPERTY(QString mimeType READ mimeType CONSTANT) Q_PROPERTY(QString mimeType READ mimeType CONSTANT)
public: public:
@@ -45,6 +49,8 @@ class FileSystemEntry : public QObject {
[[nodiscard]] qint64 size() const; [[nodiscard]] qint64 size() const;
[[nodiscard]] bool isDir() const; [[nodiscard]] bool isDir() const;
[[nodiscard]] bool isImage() const; [[nodiscard]] bool isImage() const;
[[nodiscard]] bool isVideo() const;
[[nodiscard]] QString thumbnailPath() const;
[[nodiscard]] QString mimeType() const; [[nodiscard]] QString mimeType() const;
void updateRelativePath(const QDir& dir); void updateRelativePath(const QDir& dir);
@@ -85,13 +91,16 @@ class FileSystemModel : public QAbstractListModel {
Q_PROPERTY( Q_PROPERTY(
QStringList nameFilters READ nameFilters WRITE setNameFilters NOTIFY QStringList nameFilters READ nameFilters WRITE setNameFilters NOTIFY
nameFiltersChanged) nameFiltersChanged)
Q_PROPERTY(
bool requireHardwareCodec READ requireHardwareCodec WRITE
setRequireHardwareCodec NOTIFY requireHardwareCodecChanged)
Q_PROPERTY( Q_PROPERTY(
QQmlListProperty<ZShell::models::FileSystemEntry> entries READ entries QQmlListProperty<ZShell::models::FileSystemEntry> entries READ entries
NOTIFY entriesChanged) NOTIFY entriesChanged)
public: public:
enum Filter { NoFilter, Images, Files, Dirs }; enum Filter { NoFilter, Images, Videos, Media, Files, Dirs };
Q_ENUM(Filter) Q_ENUM(Filter)
explicit FileSystemModel(QObject* parent = nullptr); explicit FileSystemModel(QObject* parent = nullptr);
@@ -123,6 +132,9 @@ class FileSystemModel : public QAbstractListModel {
[[nodiscard]] QStringList nameFilters() const; [[nodiscard]] QStringList nameFilters() const;
void setNameFilters(const QStringList& nameFilters); void setNameFilters(const QStringList& nameFilters);
[[nodiscard]] bool requireHardwareCodec() const;
void setRequireHardwareCodec(bool requireHardwareCodec);
[[nodiscard]] QQmlListProperty<FileSystemEntry> entries(); [[nodiscard]] QQmlListProperty<FileSystemEntry> entries();
signals: signals:
@@ -133,6 +145,7 @@ class FileSystemModel : public QAbstractListModel {
void sortReverseChanged(); void sortReverseChanged();
void filterChanged(); void filterChanged();
void nameFiltersChanged(); void nameFiltersChanged();
void requireHardwareCodecChanged();
void entriesChanged(); void entriesChanged();
private: private:
@@ -148,6 +161,10 @@ class FileSystemModel : public QAbstractListModel {
bool m_sortReverse; bool m_sortReverse;
Filter m_filter; Filter m_filter;
QStringList m_nameFilters; QStringList m_nameFilters;
bool m_requireHardwareCodec = false;
QHash<QString, bool> m_codecCache;
QMutex m_codecMutex;
void watchDirIfRecursive(const QString& path); void watchDirIfRecursive(const QString& path);
void update(); void update();
@@ -156,6 +173,7 @@ class FileSystemModel : public QAbstractListModel {
void updateEntriesForDir(const QString& dir); void updateEntriesForDir(const QString& dir);
void applyChanges( void applyChanges(
const QSet<QString>& removedPaths, const QSet<QString>& addedPaths); const QSet<QString>& removedPaths, const QSet<QString>& addedPaths);
[[nodiscard]] bool isHardwareVideo(const QString& path);
[[nodiscard]] bool compareEntries( [[nodiscard]] bool compareEntries(
const FileSystemEntry* a, const FileSystemEntry* b) const; const FileSystemEntry* a, const FileSystemEntry* b) const;
}; };
+876
View File
@@ -0,0 +1,876 @@
#include "wallpaperdb.hpp"
#include "wallpaperutil.hpp"
#include <QtQuick/qquickimageprovider.h>
#include <qbytearray.h>
#include <qdiriterator.h>
#include <qelapsedtimer.h>
#include <qfuturewatcher.h>
#include <qimage.h>
#include <qimagereader.h>
#include <qjsondocument.h>
#include <qjsonobject.h>
#include <qlogging.h>
#include <qqmlengine.h>
#include <qsqldatabase.h>
#include <qsqlerror.h>
#include <qsqlquery.h>
#include <qtconcurrentrun.h>
#include <quuid.h>
#include <algorithm>
#include <atomic>
#include <mutex>
Q_LOGGING_CATEGORY(dbLog, "ZShell.Internal.wallpaper.db", QtWarningMsg)
namespace ZShell::models {
class WallpaperImageProvider : public QQuickImageProvider {
public:
explicit WallpaperImageProvider(const QString& dbPath)
: QQuickImageProvider(QQuickImageProvider::Image), m_dbPath(dbPath) {}
void setDbPath(const QString& dbPath) {
std::lock_guard<std::mutex> lock(m_mutex);
if (m_dbPath == dbPath) {
return;
}
m_dbPath = dbPath;
m_generation.fetch_add(1, std::memory_order_relaxed);
}
QImage requestImage(
const QString& id, QSize* size, const QSize& requestedSize) override {
const QString path =
QString::fromUtf8(QByteArray::fromHex(id.toUtf8()));
if (path.isEmpty() || !openThreadDb(currentDbPath())) {
return {};
}
QSqlQuery query(QSqlDatabase::database(m_threadUuid));
query.prepare("SELECT cache FROM wallpapers WHERE path = ?");
query.addBindValue(path);
if (!query.exec() || !query.next()) {
return {};
}
const QByteArray bytes = query.value(0).toByteArray();
if (bytes.isEmpty()) {
return {};
}
QImage image = QImage::fromData(bytes, "JPEG");
if (image.isNull()) {
return {};
}
if (size) {
*size = image.size();
}
if (requestedSize.isValid() && !requestedSize.isEmpty() &&
(image.width() > requestedSize.width() ||
image.height() > requestedSize.height())) {
image = image.scaled(
requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
}
return image;
}
private:
QString currentDbPath() const {
std::lock_guard<std::mutex> lock(m_mutex);
return m_dbPath;
}
bool openThreadDb(const QString& dbPath) {
const quint64 generation = m_generation.load(std::memory_order_relaxed);
if (!m_threadUuid.isEmpty() && m_threadGeneration == generation) {
return true;
}
if (!m_threadUuid.isEmpty()) {
{
auto db = QSqlDatabase::database(m_threadUuid, false);
if (db.isValid()) {
db.close();
}
}
QSqlDatabase::removeDatabase(m_threadUuid);
m_threadUuid.clear();
}
if (dbPath.isEmpty()) {
return false;
}
const QString uuid = QUuid::createUuid().toString(QUuid::WithoutBraces);
auto db = QSqlDatabase::addDatabase("QSQLITE", uuid);
db.setConnectOptions("QSQLITE_OPEN_READONLY");
db.setDatabaseName(dbPath);
if (!db.open()) {
return false;
}
QSqlQuery query(db);
query.exec("PRAGMA journal_mode=WAL");
m_threadUuid = uuid;
m_threadGeneration = generation;
return true;
}
mutable std::mutex m_mutex;
QString m_dbPath;
std::atomic<quint64> m_generation = 0;
thread_local static QString m_threadUuid;
thread_local static quint64 m_threadGeneration;
};
thread_local QString WallpaperImageProvider::m_threadUuid;
thread_local quint64 WallpaperImageProvider::m_threadGeneration;
WallpaperEntry::WallpaperEntry(
const QString& path, const QString& relativePath, QObject* parent)
: QObject(parent)
, m_fileInfo(path)
, m_path(path)
, m_relativePath(relativePath) {}
QString WallpaperEntry::path() const {
return m_path;
}
QString WallpaperEntry::relativePath() const {
return m_relativePath;
}
QString WallpaperEntry::name() const {
return m_fileInfo.fileName();
}
QString WallpaperEntry::baseName() const {
return m_fileInfo.baseName();
}
QString WallpaperEntry::parentDir() const {
return m_fileInfo.absolutePath();
}
QString WallpaperEntry::suffix() const {
return m_fileInfo.completeSuffix();
}
qint64 WallpaperEntry::size() const {
return m_fileInfo.size();
}
bool WallpaperEntry::isImage() const {
return !hasVideoExtension(m_path);
}
bool WallpaperEntry::isVideo() const {
return hasVideoExtension(m_path);
}
bool WallpaperEntry::hardware() const {
return m_hardware;
}
bool WallpaperEntry::cached() const {
return !m_thumbnailPath.isEmpty();
}
QString WallpaperEntry::thumbnailPath() const {
if (!m_thumbnailPath.isEmpty()) {
return m_thumbnailPath;
}
return isImage() ? m_path : QString();
}
void WallpaperEntry::setRelativePath(const QString& path) {
if (m_relativePath == path) {
return;
}
m_relativePath = path;
emit relativePathChanged();
}
void WallpaperEntry::setHardware(bool hardware) {
if (m_hardware == hardware) {
return;
}
m_hardware = hardware;
emit hardwareChanged();
}
void WallpaperEntry::setThumbnailPath(const QString& path) {
if (m_thumbnailPath == path) {
return;
}
m_thumbnailPath = path;
emit thumbnailPathChanged();
}
WallpaperDb::WallpaperDb(QObject* parent) : QObject(parent) {
m_uuid = QUuid::createUuid().toString(QUuid::WithoutBraces);
m_dbPath = QDir::homePath() + "/.local/state/zshell/wallpapers.sqlite";
m_queueTimer.setInterval(300);
m_queueTimer.setTimerType(Qt::CoarseTimer);
QObject::connect(
&m_queueTimer, &QTimer::timeout, this, &WallpaperDb::processQueue);
m_watchTimer.setInterval(400);
m_watchTimer.setSingleShot(true);
QObject::connect(
&m_watcher, &QFileSystemWatcher::directoryChanged, this, [this]() {
m_watchTimer.start();
});
QObject::connect(
&m_watchTimer, &QTimer::timeout, this, &WallpaperDb::scanDir);
}
WallpaperDb::~WallpaperDb() {
if (QSqlDatabase::contains(m_uuid)) {
{
auto db = QSqlDatabase::database(m_uuid, false);
if (db.isValid()) {
db.close();
}
}
QSqlDatabase::removeDatabase(m_uuid);
}
}
QString WallpaperDb::path() const {
return m_path;
}
void WallpaperDb::setPath(const QString& path) {
if (m_path == path) {
return;
}
m_path = path;
m_dir = QDir(path);
emit pathChanged();
for (const auto& dir : m_watcher.directories()) {
m_watcher.removePath(dir);
}
scanDir();
}
QString WallpaperDb::dbPath() const {
return m_dbPath;
}
void WallpaperDb::setDbPath(const QString& path) {
if (m_dbPath == path) {
return;
}
m_dbPath = path;
emit dbPathChanged();
if (!m_path.isEmpty()) {
scanDir();
}
}
bool WallpaperDb::requireHardwareCodec() const {
return m_requireHardwareCodec;
}
void WallpaperDb::setRequireHardwareCodec(bool requireHardwareCodec) {
if (m_requireHardwareCodec == requireHardwareCodec) {
return;
}
m_requireHardwareCodec = requireHardwareCodec;
emit requireHardwareCodecChanged();
rebuildEntries();
}
QQmlListProperty<WallpaperEntry> WallpaperDb::entries() {
return QQmlListProperty<WallpaperEntry>(this, &m_entries);
}
QString WallpaperDb::thumbnailUrl(const QString& path) {
return QStringLiteral("image://wallpapers/") +
QString::fromUtf8(path.toUtf8().toHex());
}
QSize WallpaperDb::cacheSize() const {
return m_cacheSize;
}
void WallpaperDb::setCacheSize(const QSize& cacheSize) {
if (m_cacheSize == cacheSize) {
return;
}
const QSize prev = m_cacheSize;
const bool hadPrevious = !prev.isEmpty();
m_cacheSize = cacheSize;
emit cacheSizeChanged();
if (hadPrevious && qint64(cacheSize.width()) * cacheSize.height() >
qint64(prev.width()) * prev.height()) {
const auto db = QSqlDatabase::database(m_uuid, false);
for (auto* entry : std::as_const(m_byPath)) {
if (!entry->cached()) {
continue;
}
if (db.isOpen()) {
QSqlQuery query(db);
query.prepare(
"UPDATE wallpapers SET cache = NULL, scheme_thumb = NULL,"
" poster = NULL WHERE path = ?");
query.addBindValue(entry->path());
query.exec();
}
entry->setThumbnailPath(QString());
queueMetadata(entry, entry->isVideo());
}
rebuildEntries();
}
if (!m_queue.isEmpty() && !m_queueTimer.isActive()) {
m_queueTimer.start();
}
}
void WallpaperDb::registerProvider() {
if (m_provider != nullptr) {
m_provider->setDbPath(m_dbPath);
return;
}
auto* engine = qmlEngine(this);
if (engine == nullptr) {
return;
}
m_provider = new WallpaperImageProvider(m_dbPath);
engine->addImageProvider(QStringLiteral("wallpapers"), m_provider);
}
void WallpaperDb::openDb() {
if (m_dbPath.isEmpty()) {
return;
}
if (QSqlDatabase::contains(m_uuid)) {
{
auto db = QSqlDatabase::database(m_uuid, false);
if (db.isOpen() && db.databaseName() == m_dbPath) {
registerProvider();
return;
}
if (db.isValid()) {
db.close();
}
}
QSqlDatabase::removeDatabase(m_uuid);
}
QDir().mkpath(QFileInfo(m_dbPath).absolutePath());
auto db = QSqlDatabase::addDatabase("QSQLITE", m_uuid);
db.setDatabaseName(m_dbPath);
if (!db.open()) {
qWarning(dbLog) << "WallpaperDb: could not open database" << m_dbPath
<< db.lastError();
return;
}
QSqlQuery query(db);
query.exec("PRAGMA journal_mode=WAL");
query.exec(
"CREATE TABLE IF NOT EXISTS wallpapers("
"path TEXT PRIMARY KEY, kind TEXT, codec TEXT, hardware INTEGER,"
" size INTEGER, mtime INTEGER, cache BLOB, scheme_thumb BLOB,"
" poster BLOB, crop TEXT)");
query.exec("SELECT cache FROM wallpapers LIMIT 1");
if (query.lastError().isValid()) {
query.exec("DROP TABLE IF EXISTS wallpapers");
query.exec(
"CREATE TABLE wallpapers("
"path TEXT PRIMARY KEY, kind TEXT, codec TEXT, hardware INTEGER,"
" size INTEGER, mtime INTEGER, cache BLOB, scheme_thumb BLOB,"
" poster BLOB, crop TEXT)");
} else {
query.exec("ALTER TABLE wallpapers ADD COLUMN scheme_thumb BLOB");
query.exec("ALTER TABLE wallpapers ADD COLUMN poster BLOB");
query.exec("ALTER TABLE wallpapers ADD COLUMN crop TEXT");
}
registerProvider();
}
void WallpaperDb::scanDir() {
openDb();
watchDir(m_dir.absolutePath());
if (!m_dir.exists()) {
if (!m_entries.isEmpty()) {
m_entries.clear();
emit entriesChanged();
}
return;
}
auto* watcher = new QFutureWatcher<ScanResult>(this);
QObject::connect(
watcher, &QFutureWatcher<ScanResult>::finished, this, [this, watcher]() {
applyScan(watcher->result());
watcher->deleteLater();
});
const QDir dir = m_dir;
QHash<QString, QPair<qint64, bool>> knownCodecs;
for (const auto* entry : std::as_const(m_byPath)) {
if (entry->isVideo()) {
knownCodecs.insert(
entry->path(), qMakePair(entry->size(), entry->hardware()));
}
}
watcher->setFuture(QtConcurrent::run([dir, knownCodecs]() {
ScanResult result;
result.dir = dir;
QDirIterator it(dir.absolutePath(), QDirIterator::Subdirectories);
while (it.hasNext()) {
const QString path = it.next();
const QFileInfo info(path);
if (info.isDir()) {
continue;
}
if (hasVideoExtension(path)) {
FileEntry file;
file.info = info;
const auto known = knownCodecs.constFind(path);
if (known != knownCodecs.constEnd() &&
known.value().first == info.size()) {
file.hardware = known.value().second;
} else {
file.hardware = probeHardwareCodec(path);
}
result.files.append(file);
} else {
QImageReader reader(path);
if (reader.canRead()) {
FileEntry file;
file.info = info;
result.files.append(file);
}
}
}
return result;
}));
}
void WallpaperDb::watchDir(const QString& dir) {
if (dir.isEmpty() || m_watcher.directories().contains(dir)) {
return;
}
if (!QDir(dir).exists()) {
return;
}
m_watcher.addPath(dir);
}
void WallpaperDb::applyScan(const ScanResult& result) {
qCInfo(
dbLog,
"WallpaperDb: scan of %s found %d files (cache target %dx%d)",
m_dir.absolutePath().toUtf8().constData(),
int(result.files.size()),
m_cacheSize.width(),
m_cacheSize.height());
const auto db = QSqlDatabase::database(m_uuid, false);
QSet<QString> seen;
for (const auto& file : result.files) {
seen.insert(file.info.absoluteFilePath());
}
for (auto it = m_byPath.begin(); it != m_byPath.end();) {
if (!seen.contains(it.key())) {
if (db.isOpen()) {
QSqlQuery query(db);
query.prepare("DELETE FROM wallpapers WHERE path = ?");
query.addBindValue(it.key());
query.exec();
}
delete it.value();
m_crops.remove(it.key());
it = m_byPath.erase(it);
} else {
++it;
}
}
for (const auto& file : result.files) {
const QFileInfo& info = file.info;
const QString path = info.absoluteFilePath();
const qint64 size = info.size();
const qint64 mtime = info.lastModified().toMSecsSinceEpoch() / 1000;
const bool isVideo = hasVideoExtension(path);
watchDir(info.absolutePath());
const auto existing = m_byPath.constFind(path);
if (existing != m_byPath.constEnd()) {
if (existing.value()->size() == size) {
existing.value()->setHardware(file.hardware);
continue;
}
}
WallpaperEntry* entry = m_byPath.value(path);
if (!entry) {
entry =
new WallpaperEntry(path, m_dir.relativeFilePath(path), this);
m_byPath.insert(path, entry);
}
entry->setHardware(file.hardware);
bool reused = false;
if (db.isOpen()) {
QSqlQuery query(db);
query.prepare(
"SELECT cache, scheme_thumb, poster, size, mtime, crop"
" FROM wallpapers WHERE path = ?");
query.addBindValue(path);
query.exec();
if (query.next()) {
const auto cache = query.value(0).toByteArray();
const auto scheme = query.value(1).toByteArray();
const auto poster = query.value(2).toByteArray();
const auto storedSize = query.value(3).toLongLong();
const auto storedMtime = query.value(4).toLongLong();
loadCrop(path, query.value(5).toString());
const bool posterOk = !isVideo || !poster.isEmpty();
if (storedSize == size && storedMtime == mtime &&
!cache.isEmpty()) {
reused = true;
entry->setThumbnailPath(thumbnailUrl(path));
if (scheme.isEmpty() || !posterOk) {
queueMetadata(entry, isVideo, cache, poster);
}
}
}
}
if (!reused) {
if (db.isOpen()) {
QSqlQuery query(db);
query.prepare(
"INSERT INTO wallpapers"
" (path, kind, hardware, size, mtime, cache)"
" VALUES (?, ?, ?, ?, ?, NULL)"
" ON CONFLICT(path) DO UPDATE SET kind = excluded.kind,"
" hardware = excluded.hardware, size = excluded.size,"
" mtime = excluded.mtime, cache = NULL,"
" scheme_thumb = NULL, poster = NULL");
query.addBindValue(path);
query.addBindValue(isVideo ? "video" : "image");
query.addBindValue(
isVideo ? (file.hardware ? 1 : 0) : QVariant());
query.addBindValue(size);
query.addBindValue(mtime);
if (!query.exec()) {
qWarning(dbLog)
<< "WallpaperDb: could not store the row for" << path
<< query.lastError();
}
}
queueMetadata(entry, isVideo);
}
}
rebuildEntries();
}
void WallpaperDb::queueMetadata(
WallpaperEntry* entry,
bool isVideo,
const QByteArray& existingCache,
const QByteArray& existingPoster) {
qCInfo(
dbLog,
"WallpaperDb: queued cache job for %s (%s%s)",
entry->path().toUtf8().constData(),
isVideo ? "video" : "image",
existingCache.isEmpty() ? "" : ", existing blob");
m_queue.append(
MetadataJob{entry, entry->path(), isVideo, existingCache, existingPoster});
if (!m_queueTimer.isActive()) {
m_queueTimer.start();
}
}
void WallpaperDb::processQueue() {
if (m_jobRunning) {
return;
}
if (m_queue.isEmpty()) {
m_queueTimer.stop();
return;
}
if (m_cacheSize.isEmpty()) {
return;
}
const MetadataJob job = m_queue.takeFirst();
m_jobRunning = true;
qCInfo(
dbLog,
"WallpaperDb: caching %s (%s, %d jobs left in queue)",
job.path.toUtf8().constData(),
job.isVideo ? "video" : "image",
int(m_queue.size()));
runMetadataJob(job);
}
void WallpaperDb::runMetadataJob(const MetadataJob& job) {
struct CacheResult {
QString path;
QByteArray cache;
QByteArray poster;
QByteArray scheme;
};
const QSize cacheSize = m_cacheSize;
const bool isVideoJob = job.isVideo;
QElapsedTimer clock;
clock.start();
auto* watcher = new QFutureWatcher<CacheResult>(this);
QObject::connect(
watcher,
&QFutureWatcher<CacheResult>::finished,
this,
[this, watcher, clock, isVideoJob]() {
const auto result = watcher->result();
qCInfo(
dbLog,
"WallpaperDb: %s produced %d bytes of cache in %dms",
result.path.toUtf8().constData(),
int(result.cache.size()),
int(clock.elapsed()));
qCInfo(
dbLog,
"WallpaperDb: %s poster %d bytes",
result.path.toUtf8().constData(),
int(result.poster.size()));
if (isVideoJob && result.poster.isEmpty()) {
qWarning(dbLog)
<< "WallpaperDb: no poster produced for" << result.path;
}
if (QSqlDatabase::contains(m_uuid)) {
const auto db = QSqlDatabase::database(m_uuid, false);
if (db.isOpen()) {
QSqlQuery query(db);
query.prepare(
"UPDATE wallpapers SET cache = ?, scheme_thumb = ?,"
" poster = ? WHERE path = ?");
query.addBindValue(result.cache);
query.addBindValue(result.scheme);
query.addBindValue(result.poster);
query.addBindValue(result.path);
if (!query.exec()) {
qWarning(dbLog)
<< "WallpaperDb: could not store the cache for"
<< result.path << query.lastError();
}
}
}
if (!result.cache.isEmpty()) {
if (auto* entry = m_byPath.value(result.path)) {
entry->setThumbnailPath(thumbnailUrl(result.path));
}
emit cacheStored(result.path);
}
m_jobRunning = false;
rebuildEntries();
watcher->deleteLater();
});
watcher->setFuture(QtConcurrent::run([job, cacheSize]() {
CacheResult result;
result.path = job.path;
QByteArray cache = job.existingCache;
if (cache.isEmpty()) {
cache = job.isVideo ? videoCacheBytes(job.path, cacheSize)
: imageCacheBytes(job.path, cacheSize);
}
QByteArray poster = job.existingPoster;
if (poster.isEmpty()) {
poster =
job.isVideo ? videoPosterBytes(job.path, cacheSize) : cache;
}
result.cache = cache;
result.poster = poster;
result.scheme = cache.isEmpty() ? QByteArray()
: schemeThumbBytes(cache, 128);
return result;
}));
}
void WallpaperDb::rebuildEntries() {
QList<WallpaperEntry*> next;
next.reserve(m_byPath.size());
for (auto* entry : std::as_const(m_byPath)) {
if (entryAllowed(entry)) {
next.append(entry);
}
}
std::sort(
next.begin(),
next.end(),
[](const WallpaperEntry* a, const WallpaperEntry* b) {
return a->relativePath() < b->relativePath();
});
if (next == m_entries) {
return;
}
m_entries = next;
emit entriesChanged();
}
bool WallpaperDb::entryAllowed(const WallpaperEntry* entry) const {
if (!entry->isVideo()) {
return true;
}
if (!m_requireHardwareCodec) {
return true;
}
return entry->hardware();
}
QVariantMap WallpaperDb::crop(const QString& path, const QString& screen) const {
const auto pathCrops = m_crops.constFind(path);
if (pathCrops == m_crops.constEnd()) return {};
return pathCrops.value().value(screen).toMap();
}
void WallpaperDb::setCrop(
const QString& path, const QString& screen, const QVariantMap& crop) {
if (path.isEmpty() || screen.isEmpty()) return;
QVariantMap& pathCrops = m_crops[path];
pathCrops[screen] = crop;
storeCropJson(path);
emit cropsChanged(path);
}
void WallpaperDb::loadCrop(const QString& path, const QString& json) {
if (json.isEmpty()) {
m_crops.remove(path);
return;
}
const auto doc = QJsonDocument::fromJson(json.toUtf8());
if (!doc.isObject()) {
m_crops.remove(path);
return;
}
m_crops[path] = doc.object().toVariantMap();
}
void WallpaperDb::storeCropJson(const QString& path) {
const auto db = QSqlDatabase::database(m_uuid, false);
if (!db.isOpen()) return;
const auto pathCrops = m_crops.value(path);
const QByteArray json =
pathCrops.isEmpty()
? QByteArray()
: QJsonDocument(QJsonObject::fromVariantMap(pathCrops))
.toJson(QJsonDocument::Compact);
QSqlQuery query(db);
query.prepare("UPDATE wallpapers SET crop = ? WHERE path = ?");
query.addBindValue(json.isEmpty() ? QVariant() : QVariant(json));
query.addBindValue(path);
if (!query.exec()) {
qWarning(dbLog) << "WallpaperDb: could not store the crop for" << path
<< query.lastError();
}
}
} // namespace ZShell::models
+188
View File
@@ -0,0 +1,188 @@
#pragma once
#include <qbytearray.h>
#include <qdir.h>
#include <qfileinfo.h>
#include <qfilesystemwatcher.h>
#include <qhash.h>
#include <qlist.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qqmllist.h>
#include <qsize.h>
#include <qtimer.h>
#include <qvariant.h>
#include <qvector.h>
namespace ZShell::models {
class WallpaperImageProvider;
class WallpaperEntry : public QObject {
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE(
"WallpaperEntry instances can only be retrieved from a WallpaperDb")
Q_PROPERTY(QString path READ path CONSTANT)
Q_PROPERTY(QString relativePath READ relativePath NOTIFY relativePathChanged)
Q_PROPERTY(QString name READ name CONSTANT)
Q_PROPERTY(QString baseName READ baseName CONSTANT)
Q_PROPERTY(QString parentDir READ parentDir CONSTANT)
Q_PROPERTY(QString suffix READ suffix CONSTANT)
Q_PROPERTY(qint64 size READ size CONSTANT)
Q_PROPERTY(bool isImage READ isImage CONSTANT)
Q_PROPERTY(bool isVideo READ isVideo CONSTANT)
Q_PROPERTY(bool hardware READ hardware NOTIFY hardwareChanged)
Q_PROPERTY(
QString thumbnailPath READ thumbnailPath NOTIFY thumbnailPathChanged)
public:
explicit WallpaperEntry(
const QString& path,
const QString& relativePath,
QObject* parent = nullptr);
[[nodiscard]] QString path() const;
[[nodiscard]] QString relativePath() const;
[[nodiscard]] QString name() const;
[[nodiscard]] QString baseName() const;
[[nodiscard]] QString parentDir() const;
[[nodiscard]] QString suffix() const;
[[nodiscard]] qint64 size() const;
[[nodiscard]] bool isImage() const;
[[nodiscard]] bool isVideo() const;
[[nodiscard]] bool hardware() const;
[[nodiscard]] bool cached() const;
[[nodiscard]] QString thumbnailPath() const;
void setRelativePath(const QString& path);
void setHardware(bool hardware);
void setThumbnailPath(const QString& path);
signals:
void relativePathChanged();
void hardwareChanged();
void thumbnailPathChanged();
private:
const QFileInfo m_fileInfo;
const QString m_path;
QString m_relativePath;
QString m_thumbnailPath;
bool m_hardware = false;
};
class WallpaperDb : public QObject {
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged)
Q_PROPERTY(QString dbPath READ dbPath WRITE setDbPath NOTIFY dbPathChanged)
Q_PROPERTY(
bool requireHardwareCodec READ requireHardwareCodec WRITE
setRequireHardwareCodec NOTIFY requireHardwareCodecChanged)
Q_PROPERTY(
QQmlListProperty<ZShell::models::WallpaperEntry> entries READ entries
NOTIFY entriesChanged)
Q_PROPERTY(
QSize cacheSize READ cacheSize WRITE setCacheSize NOTIFY
cacheSizeChanged)
public:
explicit WallpaperDb(QObject* parent = nullptr);
~WallpaperDb() override;
[[nodiscard]] QString path() const;
void setPath(const QString& path);
[[nodiscard]] QString dbPath() const;
void setDbPath(const QString& path);
[[nodiscard]] bool requireHardwareCodec() const;
void setRequireHardwareCodec(bool requireHardwareCodec);
[[nodiscard]] QQmlListProperty<WallpaperEntry> entries();
[[nodiscard]] QSize cacheSize() const;
void setCacheSize(const QSize& cacheSize);
[[nodiscard]] static QString thumbnailUrl(const QString& path);
Q_INVOKABLE QVariantMap crop(const QString& path, const QString& screen)
const;
Q_INVOKABLE void setCrop(
const QString& path, const QString& screen, const QVariantMap& crop);
signals:
void pathChanged();
void dbPathChanged();
void requireHardwareCodecChanged();
void entriesChanged();
void cacheSizeChanged();
void cacheStored(const QString& path);
void cropsChanged(const QString& path);
private:
struct FileEntry {
QFileInfo info;
bool hardware = false;
};
struct ScanResult {
QVector<FileEntry> files;
QDir dir;
};
struct MetadataJob {
WallpaperEntry* entry;
QString path;
bool isVideo;
QByteArray existingCache;
QByteArray existingPoster;
};
void openDb();
void registerProvider();
void scanDir();
void watchDir(const QString& dir);
void applyScan(const ScanResult& result);
void queueMetadata(
WallpaperEntry* entry,
bool isVideo,
const QByteArray& existingCache = {},
const QByteArray& existingPoster = {});
void processQueue();
void runMetadataJob(const MetadataJob& job);
void rebuildEntries();
void loadCrop(const QString& path, const QString& json);
void storeCropJson(const QString& path);
[[nodiscard]] bool entryAllowed(const WallpaperEntry* entry) const;
QDir m_dir;
QString m_path;
QString m_dbPath;
QString m_uuid;
WallpaperImageProvider* m_provider = nullptr;
bool m_requireHardwareCodec = false;
QSize m_cacheSize;
QList<WallpaperEntry*> m_entries;
QHash<QString, WallpaperEntry*> m_byPath;
QHash<QString, QVariantMap> m_crops;
QVector<MetadataJob> m_queue;
QTimer m_queueTimer;
bool m_jobRunning = false;
QFileSystemWatcher m_watcher;
QTimer m_watchTimer;
};
} // namespace ZShell::models
+398
View File
@@ -0,0 +1,398 @@
#pragma once
#include <qbuffer.h>
#include <qfile.h>
#include <qfileinfo.h>
#include <qimage.h>
#include <qimagereader.h>
#include <qprocess.h>
#include <qstring.h>
#include <qstringlist.h>
#include <qtemporarydir.h>
#include <algorithm>
#include <cstring>
namespace ZShell::models {
namespace {
inline const QStringList kVideoExtensions = {
"mp4",
"mkv",
"webm",
"avi",
"mov",
"wmv",
"flv",
"m4v",
"mpg",
"mpeg",
"3gp",
"ogv",
"ts",
"vob",
};
inline bool hasVideoExtension(const QString& path) {
return kVideoExtensions.contains(QFileInfo(path).suffix().toLower());
}
inline const QStringList kHardwareCodecs = {
"av1",
"hevc",
"h264",
"vp9",
"vp8",
"vc1",
"mpeg2",
"mpeg2video",
"mpeg4",
"mjpeg",
};
inline bool probeHardwareCodec(const QString& path) {
QProcess process;
process.start(
"ffprobe",
{
"-v",
"error",
"-select_streams",
"v:0",
"-show_entries",
"stream=codec_name",
"-of",
"default=nw=1:nk=1",
path,
});
process.waitForFinished(5000);
if (process.exitCode() != 0) {
return false;
}
const QString codec =
QString::fromUtf8(process.readAllStandardOutput()).trimmed().toLower();
return kHardwareCodecs.contains(codec);
}
inline QSize coverSizeNoUpscale(const QSize& source, const QSize& fit) {
if (!source.isValid() || source.isEmpty() || !fit.isValid() ||
fit.isEmpty()) {
return source;
}
const QSize cover = source.scaled(fit, Qt::KeepAspectRatioByExpanding);
if (cover.width() <= source.width() && cover.height() <= source.height()) {
return cover;
}
return source;
}
inline QImage cleanImage(const QImage& src) {
QImage out(src.size(), QImage::Format_RGB888);
if (out.isNull()) {
return out;
}
const auto rowBytes =
static_cast<size_t>(std::min(src.bytesPerLine(), out.bytesPerLine()));
for (int y = 0; y < src.height(); ++y) {
std::memcpy(out.scanLine(y), src.constScanLine(y), rowBytes);
}
return out;
}
inline QByteArray imageCacheBytes(const QString& path, const QSize& fit) {
QImageReader reader(path);
reader.setAutoTransform(true);
const QSize source = reader.size();
const QSize target = coverSizeNoUpscale(source, fit);
if (target != source && !target.isEmpty()) {
reader.setScaledSize(target);
}
QImage image = reader.read();
if (image.isNull()) {
return {};
}
image = cleanImage(image.convertToFormat(QImage::Format_RGB888));
QByteArray bytes;
QBuffer buffer(&bytes);
if (!buffer.open(QIODevice::WriteOnly) ||
!image.save(&buffer, "JPEG", 90)) {
return {};
}
return bytes;
}
inline QByteArray videoCacheBytes(const QString& src, const QSize& fit) {
auto runProcess = [](const QString& program, const QStringList& args) {
QProcess process;
process.start(program, args);
if (!process.waitForFinished(15000)) {
process.kill();
process.waitForFinished(1000);
}
return qMakePair(process.exitCode(), process.readAllStandardOutput());
};
double duration = 0.0;
const auto probe = runProcess(
"ffprobe",
{
"-v",
"error",
"-show_entries",
"format=duration",
"-of",
"default=nw=1:nk=1",
src,
});
if (probe.first == 0) {
duration = QString::fromUtf8(probe.second).trimmed().toDouble();
}
QSize source;
const auto resProbe = runProcess(
"ffprobe",
{
"-v",
"error",
"-select_streams",
"v:0",
"-show_entries",
"stream=width,height",
"-of",
"csv=s=x:p=0",
src,
});
if (resProbe.first == 0) {
const auto parts =
QString::fromUtf8(resProbe.second).trimmed().split('x');
if (parts.size() == 2) {
source = QSize(parts[0].toInt(), parts[1].toInt());
}
}
const int samples = duration > 0 ? std::min(10, std::max(4, int(duration)))
: 8;
QSize target = coverSizeNoUpscale(source, fit);
if (target.isEmpty()) {
target = fit;
}
target.setWidth(target.width() & ~1);
target.setHeight(target.height() & ~1);
const QString scale =
QStringLiteral(
"scale=%1:%2:force_original_aspect_ratio=decrease,"
"scale=trunc(iw/2)*2:trunc(ih/2)*2")
.arg(target.width())
.arg(target.height());
QTemporaryDir tmpDir;
if (!tmpDir.isValid()) {
return {};
}
int bestIndex = -1;
double bestChroma = -1.0;
double bestLuma = -1.0;
for (int i = 0; i < samples; ++i) {
const double frac = duration > 0 ? (i + 0.5) / samples : 0.0;
const QString seek = duration > 0
? QString::number(frac * duration, 'f', 3)
: QStringLiteral("0");
const QString framePath =
tmpDir.filePath(QStringLiteral("frame_%1.jpg").arg(i));
runProcess(
"ffmpeg",
{
"-hide_banner",
"-loglevel",
"error",
"-ss",
seek,
"-i",
src,
"-frames:v",
"1",
"-vf",
scale,
"-pix_fmt",
"yuvj420p",
"-q:v",
"2",
"-y",
framePath,
});
QImage frame(framePath);
if (frame.isNull()) {
continue;
}
frame = frame.convertToFormat(QImage::Format_RGB888);
double chroma = 0.0;
double luma = 0.0;
int count = 0;
for (int y = 0; y < frame.height(); ++y) {
const auto* line = frame.constScanLine(y);
for (int x = 0; x < frame.width(); ++x) {
const int r = line[x * 3];
const int g = line[x * 3 + 1];
const int b = line[x * 3 + 2];
chroma += std::max({r, g, b}) - std::min({r, g, b});
luma += r + g + b;
++count;
}
}
const int n = std::max(1, count);
chroma /= n;
luma /= 3.0 * n;
if (chroma > bestChroma + 0.5 ||
(chroma > bestChroma - 0.5 && luma > bestLuma)) {
bestChroma = chroma;
bestLuma = luma;
bestIndex = i;
}
}
if (bestIndex < 0) {
return {};
}
QFile frame(tmpDir.filePath(QStringLiteral("frame_%1.jpg").arg(bestIndex)));
if (!frame.open(QIODevice::ReadOnly)) {
return {};
}
return frame.readAll();
}
inline QByteArray videoPosterBytes(const QString& src, const QSize& fit) {
auto runProcess = [](const QString& program, const QStringList& args) {
QProcess process;
process.start(program, args);
if (!process.waitForFinished(15000)) {
process.kill();
process.waitForFinished(1000);
}
return qMakePair(process.exitCode(), process.readAllStandardOutput());
};
QSize source;
const auto resProbe = runProcess(
"ffprobe",
{
"-v",
"error",
"-select_streams",
"v:0",
"-show_entries",
"stream=width,height",
"-of",
"csv=s=x:p=0",
src,
});
if (resProbe.first == 0) {
const auto parts =
QString::fromUtf8(resProbe.second).trimmed().split('x');
if (parts.size() == 2) {
source = QSize(parts[0].toInt(), parts[1].toInt());
}
}
QSize target = coverSizeNoUpscale(source, fit);
if (target.isEmpty()) {
target = fit;
}
target.setWidth(target.width() & ~1);
target.setHeight(target.height() & ~1);
const QString scale =
QStringLiteral(
"scale=%1:%2:force_original_aspect_ratio=decrease,"
"scale=trunc(iw/2)*2:trunc(ih/2)*2")
.arg(target.width())
.arg(target.height());
QTemporaryDir tmpDir;
if (!tmpDir.isValid()) {
return {};
}
const QString framePath = tmpDir.filePath(QStringLiteral("poster.jpg"));
runProcess(
"ffmpeg",
{
"-hide_banner",
"-loglevel",
"error",
"-ss",
"0",
"-i",
src,
"-frames:v",
"1",
"-vf",
scale,
"-pix_fmt",
"yuvj420p",
"-q:v",
"2",
"-y",
framePath,
});
QFile frame(framePath);
if (!frame.open(QIODevice::ReadOnly)) {
return {};
}
return frame.readAll();
}
inline QByteArray schemeThumbBytes(const QByteArray& source, int maxSide) {
QImage image = QImage::fromData(source, "JPEG");
if (image.isNull()) {
return {};
}
if (image.width() > maxSide || image.height() > maxSide) {
image = image.scaled(
maxSide, maxSide, Qt::KeepAspectRatio, Qt::SmoothTransformation);
}
image = cleanImage(image.convertToFormat(QImage::Format_RGB888));
QByteArray bytes;
QBuffer buffer(&bytes);
if (!buffer.open(QIODevice::WriteOnly) ||
!image.save(&buffer, "JPEG", 85)) {
return {};
}
return bytes;
}
} // namespace
} // namespace ZShell::models
+3
View File
@@ -23,11 +23,14 @@ qml_module(ZShell-services
lyriccandidate.hpp lyriccandidate.cpp lyriccandidate.hpp lyriccandidate.cpp
lyrics.hpp lyrics.cpp lyrics.hpp lyrics.cpp
dropimporter.hpp dropimporter.cpp dropimporter.hpp dropimporter.cpp
notificationstore.hpp notificationstore.cpp
LIBRARIES LIBRARIES
Qt6::Core Qt6::Core
Qt6::Qml Qt6::Qml
Qt6::Quick
Qt6::Concurrent Qt6::Concurrent
Qt6::Network Qt6::Network
Qt6::Sql
PkgConfig::Pipewire PkgConfig::Pipewire
PkgConfig::Aubio PkgConfig::Aubio
PkgConfig::Cava PkgConfig::Cava
+1 -1
View File
@@ -93,7 +93,7 @@ int DropImporter::importAttachments(
saveBytes(id, rawImage); saveBytes(id, rawImage);
} else if (const QString src = firstImgSrc(html); !src.isEmpty()) { } else if (const QString src = firstImgSrc(html); !src.isEmpty()) {
if (src.startsWith(QLatin1String("data:"), Qt::CaseInsensitive)) { if (src.startsWith(QLatin1String("data:"), Qt::CaseInsensitive)) {
const int comma = src.indexOf(QLatin1Char(',')); const qsizetype comma = src.indexOf(QLatin1Char(','));
if (comma < 0) { if (comma < 0) {
failWith(id, tr("Malformed data URI")); failWith(id, tr("Malformed data URI"));
} else { } else {
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,248 @@
#pragma once
#include <QDateTime>
#include <QHash>
#include <QJSValue>
#include <QList>
#include <QObject>
#include <QPointer>
#include <qqmlintegration.h>
#include <QQmlListProperty>
#include <QSet>
#include <QString>
#include <QTimer>
namespace ZShell::config {
class Notifs;
}
namespace ZShell::Services {
class NotificationStore;
class NotifImageProvider;
class NotificationActionRow : public QObject {
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE(
"NotificationActionRow instances come from a NotificationStore")
Q_PROPERTY(bool invokable READ invokable NOTIFY invokableChanged)
Q_PROPERTY(QString identifier READ identifier CONSTANT)
Q_PROPERTY(QString text READ text CONSTANT)
public:
NotificationActionRow(
QObject* action,
const QString& identifier,
const QString& text,
QObject* parent = nullptr);
[[nodiscard]] bool invokable() const;
[[nodiscard]] QString identifier() const;
[[nodiscard]] QString text() const;
Q_INVOKABLE void invoke();
signals:
void invokableChanged();
private:
QPointer<QObject> m_action;
QString m_identifier;
QString m_text;
};
class NotificationRow : public QObject {
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("NotificationRow instances come from a NotificationStore")
Q_PROPERTY(QQmlListProperty<NotificationActionRow> actions READ actions NOTIFY actionsChanged)
Q_PROPERTY(QString appIcon READ appIcon NOTIFY appIconChanged)
Q_PROPERTY(QString appName READ appName NOTIFY appNameChanged)
Q_PROPERTY(QString body READ body NOTIFY bodyChanged)
Q_PROPERTY(bool closed READ closed NOTIFY closedChanged)
Q_PROPERTY(bool hasActionIcons READ hasActionIcons NOTIFY hasActionIconsChanged)
Q_PROPERTY(QString image READ image NOTIFY imageChanged)
Q_PROPERTY(QString imageSource READ imageSource NOTIFY imageSourceChanged)
Q_PROPERTY(int notificationId READ notificationId CONSTANT)
Q_PROPERTY(int progress READ progress NOTIFY progressChanged)
Q_PROPERTY(bool popup READ popup WRITE setPopup NOTIFY popupChanged)
Q_PROPERTY(bool resident READ resident NOTIFY residentChanged)
Q_PROPERTY(QString summary READ summary NOTIFY summaryChanged)
Q_PROPERTY(QDateTime time READ time CONSTANT)
Q_PROPERTY(QString timeStr READ timeStr NOTIFY timeStrChanged)
Q_PROPERTY(int urgency READ urgency NOTIFY urgencyChanged)
public:
explicit NotificationRow(QObject* parent = nullptr);
[[nodiscard]] QQmlListProperty<NotificationActionRow> actions();
[[nodiscard]] QString appIcon() const;
[[nodiscard]] QString appName() const;
[[nodiscard]] QString body() const;
[[nodiscard]] bool closed() const;
[[nodiscard]] bool hasActionIcons() const;
[[nodiscard]] QString image() const;
[[nodiscard]] QString imageSource() const;
[[nodiscard]] int notificationId() const;
[[nodiscard]] int progress() const;
[[nodiscard]] bool popup() const;
void setPopup(bool popup);
[[nodiscard]] bool resident() const;
[[nodiscard]] QString summary() const;
[[nodiscard]] QDateTime time() const;
[[nodiscard]] QString timeStr() const;
[[nodiscard]] int urgency() const;
Q_INVOKABLE void close();
Q_INVOKABLE void lock(QObject* item);
Q_INVOKABLE void pausePopup();
Q_INVOKABLE void resumePopup();
Q_INVOKABLE void unlock(QObject* item);
signals:
void actionsChanged();
void appIconChanged();
void appNameChanged();
void bodyChanged();
void closedChanged();
void hasActionIconsChanged();
void imageChanged();
void imageSourceChanged();
void popupChanged();
void progressChanged();
void residentChanged();
void summaryChanged();
void timeStrChanged();
void urgencyChanged();
private:
friend NotificationStore;
[[nodiscard]] bool expired(qint64 now) const;
void refreshTimeStr();
QList<NotificationActionRow*> m_actions;
NotificationStore* m_store = nullptr;
QString m_appIcon;
QString m_appName;
QString m_body;
QString m_digest;
QString m_image;
QString m_imageSource;
QString m_summary;
QString m_timeStr;
qint64 m_deadline = 0;
qint64 m_rowId = -1;
qint64 m_timeMs = 0;
QPointer<QObject> m_notification;
QSet<QObject*> m_locks;
QJSValue m_jsNotification;
bool m_closed = false;
bool m_hasActionIcons = false;
bool m_liveConnected = false;
bool m_popup = false;
bool m_popupPaused = false;
bool m_resident = false;
int m_notificationId = 0;
int m_progress = 0;
int m_urgency = 1;
};
class NotificationStore : public QObject {
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(bool autoExpire READ autoExpire WRITE setAutoExpire NOTIFY autoExpireChanged)
Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged REQUIRED)
Q_PROPERTY(int popupTimeout READ popupTimeout WRITE setPopupTimeout NOTIFY
popupTimeoutChanged)
Q_PROPERTY(QQmlListProperty<NotificationRow> rows READ rows NOTIFY rowsChanged)
public:
explicit NotificationStore(QObject* parent = nullptr);
~NotificationStore() override;
[[nodiscard]] QString path() const;
void setPath(const QString& path);
[[nodiscard]] bool autoExpire() const;
void setAutoExpire(bool autoExpire);
[[nodiscard]] int popupTimeout() const;
void setPopupTimeout(int timeout);
[[nodiscard]] QQmlListProperty<NotificationRow> rows();
Q_INVOKABLE NotificationRow* insert(const QJSValue& notification, bool popup);
Q_INVOKABLE void clear();
signals:
void autoExpireChanged();
void pathChanged();
void popupTimeoutChanged();
void rowsChanged();
private slots:
void handleLiveUpdate();
private:
friend NotificationRow;
void attachNotification(
NotificationRow* row, QObject* notification, const QJSValue& js);
void applyNotifConfig(const config::Notifs* notifs);
void closeDb();
void copyFields(NotificationRow* row, QObject* notification);
void handleClosed(NotificationRow* row);
void insertDbRow(NotificationRow* row);
void loadRows();
void migrateLegacyJson();
[[nodiscard]] bool openDb();
void orphanCleanup();
void persistPopup(const NotificationRow* row);
void prune();
void registerProvider();
void removeRow(NotificationRow* row);
// Coalesced rowsChanged: a burst of inserts/removes triggers one QML
// rebind on the next event loop turn instead of one per notification.
void scheduleRowsChanged();
[[nodiscard]] NotificationRow* rowForNotificationId(int id) const;
void startImageCache(NotificationRow* row);
void storeImageBlob(
const QPointer<NotificationRow>& row,
const QString& digest,
const QByteArray& png);
void tick();
void trimRows();
void updateDbRow(NotificationRow* row);
static QList<NotificationActionRow*> parseActions(
const QJSValue& actions, NotificationRow* row);
int m_popupTimeout = 5000;
qint64 m_nextPrune = 0;
qint64 m_nextTimeStrRefresh = 0;
bool m_autoExpire = true;
bool m_clearing = false;
bool m_rowsSignalPending = false;
QString m_uuid;
QString m_path;
QList<NotificationRow*> m_rows;
QHash<QObject*, NotificationRow*> m_liveRows;
NotifImageProvider* m_provider = nullptr;
QTimer* m_tick = nullptr;
};
} // namespace ZShell::Services
+70
View File
@@ -0,0 +1,70 @@
wl_proto(wlp-layer-shell wlr-layer-shell-unstable-v1 "${CMAKE_CURRENT_SOURCE_DIR}/protocols")
wl_proto(wlp-viewporter viewporter "${WAYLAND_PROTOCOLS}/stable/viewporter")
wl_proto(wlp-fractional-scale fractional-scale-v1 "${WAYLAND_PROTOCOLS}/staging/fractional-scale")
wl_proto(wlp-xdg-shell xdg-shell "${WAYLAND_PROTOCOLS}/stable/xdg-shell")
qml_module(ZShell-Wallpaper
URI ZShell.Wallpaper
INCLUDE_PREFIX wallpaper
SOURCES
wallpaperservice.hpp wallpaperservice.cpp
wallpaperobject.hpp wallpaperobject.cpp
wallpapersurface.hpp wallpapersurface.cpp
wallpaperimage.hpp wallpaperimage.cpp
wallpapercache.hpp wallpapercache.cpp
videotranscoder.hpp videotranscoder.cpp
videodecoder.hpp videodecoder.cpp
nvidiaplayer.hpp nvidiaplayer.cpp
vaapiplayer.hpp vaapiplayer.cpp
LIBRARIES
Qt::Gui
Qt::Quick
Qt::Concurrent
Qt::Sql
Qt::Core
Qt::GuiPrivate
Qt::WaylandClient
Qt::WaylandClientPrivate
wlp-layer-shell
wlp-xdg-shell
wlp-viewporter
wlp-fractional-scale
PkgConfig::EGL
PkgConfig::WAYLAND_EGL
PkgConfig::GL
PkgConfig::MPV
PkgConfig::AVCODEC
PkgConfig::AVFORMAT
PkgConfig::AVUTIL
ZShell-config
)
target_compile_definitions(ZShell-Wallpaper PRIVATE ZHAVE_FFMPEG=1)
if(LIBVA_FOUND)
target_link_libraries(ZShell-Wallpaper PRIVATE PkgConfig::LIBVA)
target_compile_definitions(ZShell-Wallpaper PRIVATE ZHAVE_VAAPI=1)
endif()
if(CUDAToolkit_FOUND)
target_link_libraries(ZShell-Wallpaper PRIVATE CUDA::cuda_driver)
target_compile_definitions(ZShell-Wallpaper PRIVATE ZHAVE_VIDEO=1)
endif()
qt_add_resources(ZShell-Wallpaper "wallpaper_shaders"
PREFIX "/"
FILES
shaders/wallpaper.vert
shaders/wallpaper.frag
shaders/wallpaper_video.frag
shaders/wallpaper_trans_bowtie.frag
shaders/wallpaper_trans_fade.frag
shaders/wallpaper_trans_linear_blur.frag
shaders/wallpaper_trans_window_slices.frag
shaders/wallpaper_trans_wipe_right.frag
shaders/wallpaper_trans_wipe_down.frag
shaders/wallpaper_trans_tiles_wave.frag
shaders/wallpaper_trans_split_slide_in_horizontal.frag
shaders/wallpaper_trans_hexagonalize.frag
shaders/wallpaper_trans_doom_melt.frag
)
+567
View File
@@ -0,0 +1,567 @@
#ifdef ZHAVE_VIDEO
#include "nvidiaplayer.hpp"
#define EGL_EGLEXT_PROTOTYPES
#define GL_GLEXT_PROTOTYPES
#include <EGL/egl.h>
#include <GL/gl.h>
#include <GL/glext.h>
#include <cuda.h>
#include <algorithm>
#include <chrono>
#include <climits>
#include <QtLogging>
extern "C" CUresult cuGraphicsGLRegisterImage(
CUgraphicsResource* resource,
GLuint image,
GLenum target,
unsigned int flags);
Q_LOGGING_CATEGORY(videoLog, "ZShell.Internal.wallpaper")
namespace ZShell::wallpaper {
namespace {
enum AVPixelFormat chooseFormat(
AVCodecContext* ctx, const AVPixelFormat* pix_fmts) {
(void)ctx;
for (const AVPixelFormat* p = pix_fmts; *p != AV_PIX_FMT_NONE; ++p)
if (*p == AV_PIX_FMT_CUDA) return AV_PIX_FMT_CUDA;
return pix_fmts[0];
}
class CudaCtx {
public:
explicit CudaCtx(CUcontext ctx) : m_ctx(ctx) {
if (m_ctx) cuCtxPushCurrent(m_ctx);
}
~CudaCtx() {
if (m_ctx) cuCtxPopCurrent(&m_prev);
}
private:
CUcontext m_ctx;
CUcontext m_prev = nullptr;
};
} // namespace
NvidiaPlayer::NvidiaPlayer(EGLDisplay display) {
(void)display;
}
NvidiaPlayer::~NvidiaPlayer() {
stop();
std::lock_guard<std::mutex> lock(m_mtx);
CUcontext prev = nullptr;
if (m_cuCtx) cuCtxPushCurrent(m_cuCtx);
if (m_pending) {
av_frame_unref(m_pending);
av_frame_free(&m_pending);
}
if (m_displayed) {
av_frame_unref(m_displayed);
av_frame_free(&m_displayed);
}
if (m_resY) {
cuGraphicsUnregisterResource(m_resY);
m_resY = nullptr;
}
if (m_resUV) {
cuGraphicsUnregisterResource(m_resUV);
m_resUV = nullptr;
}
if (m_texY) {
glDeleteTextures(1, &m_texY);
m_texY = 0;
}
if (m_texUV) {
glDeleteTextures(1, &m_texUV);
m_texUV = 0;
}
if (m_cuCtx) cuCtxPopCurrent(&prev);
if (m_cuCtx) {
cuCtxDestroy(m_cuCtx);
m_cuCtx = nullptr;
}
m_texSize = QSize();
}
bool NvidiaPlayer::available() {
if (cuInit(0) != CUDA_SUCCESS) return false;
CUdevice dev = 0;
return cuDeviceGet(&dev, 0) == CUDA_SUCCESS;
}
bool NvidiaPlayer::openDecoder(const QString& path) {
if (avformat_open_input(
&m_fmt, path.toUtf8().constData(), nullptr, nullptr) < 0) {
qCWarning(
videoLog,
"VideoPlayer: could not open %s",
path.toUtf8().constData());
return false;
}
if (avformat_find_stream_info(m_fmt, nullptr) < 0) {
qCWarning(videoLog, "VideoPlayer: could not read stream info");
return false;
}
m_streamIndex = -1;
for (unsigned i = 0; i < m_fmt->nb_streams; ++i)
if (m_fmt->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
m_streamIndex = int(i);
break;
}
if (m_streamIndex < 0) {
qCWarning(videoLog, "VideoPlayer: no video stream");
return false;
}
AVCodecParameters* par = m_fmt->streams[m_streamIndex]->codecpar;
const AVCodec* codec = avcodec_find_decoder(par->codec_id);
const AVCodec* hwcodec = nullptr;
void* it = nullptr;
while (const AVCodec* c = av_codec_iterate(&it)) {
if (c->id != par->codec_id || !av_codec_is_decoder(c)) continue;
for (int i = 0;
const AVCodecHWConfig* cfg = avcodec_get_hw_config(c, i);
++i)
if (cfg->device_type == AV_HWDEVICE_TYPE_CUDA &&
(cfg->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX)) {
hwcodec = c;
break;
}
if (hwcodec) break;
}
if (hwcodec) codec = hwcodec;
if (!codec) {
qCWarning(
videoLog,
"VideoPlayer: no decoder for codec id %d",
int(par->codec_id));
return false;
}
m_codec = avcodec_alloc_context3(codec);
if (!m_codec) return false;
if (avcodec_parameters_to_context(m_codec, par) < 0) {
qCWarning(videoLog, "VideoPlayer: could not copy codec params");
return false;
}
m_hwDeviceCtx = av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_CUDA);
if (!m_hwDeviceCtx) {
qCWarning(
videoLog, "VideoPlayer: could not allocate cuda hwdevice context");
return false;
}
AVCUDADeviceContext* cudaHw = reinterpret_cast<AVCUDADeviceContext*>(
reinterpret_cast<AVHWDeviceContext*>(m_hwDeviceCtx->data)->hwctx);
if (m_cuCtx) cudaHw->cuda_ctx = m_cuCtx;
if (av_hwdevice_ctx_init(m_hwDeviceCtx) < 0) {
qCWarning(
videoLog, "VideoPlayer: could not create cuda hwdevice context");
av_buffer_unref(&m_hwDeviceCtx);
return false;
}
m_codec->hw_device_ctx = av_buffer_ref(m_hwDeviceCtx);
m_codec->get_format = chooseFormat;
if (avcodec_open2(m_codec, codec, nullptr) < 0) {
qCWarning(videoLog, "VideoPlayer: could not open codec");
return false;
}
return true;
}
void NvidiaPlayer::closeDecoder() {
if (m_codec) {
avcodec_free_context(&m_codec);
m_codec = nullptr;
}
if (m_hwDeviceCtx) {
av_buffer_unref(&m_hwDeviceCtx);
m_hwDeviceCtx = nullptr;
}
if (m_fmt) {
avformat_close_input(&m_fmt);
m_fmt = nullptr;
}
m_streamIndex = -1;
}
void NvidiaPlayer::decodeLoop(const QString& path, uint32_t generation) {
if (!m_threadRunning.load(std::memory_order_acquire) ||
m_generation.load(std::memory_order_acquire) != generation)
return;
if (!openDecoder(path)) {
m_threadRunning.store(false);
return;
}
AVRational fr = m_fmt->streams[m_streamIndex]->avg_frame_rate;
if (fr.num <= 0 || fr.den <= 0) fr = m_codec->framerate;
if (fr.num <= 0 || fr.den <= 0) fr = AVRational{30, 1};
const int64_t intervalUs =
av_rescale_q(1, AVRational{fr.den, fr.num}, AVRational{1, 1000000});
AVFrame* frame = av_frame_alloc();
if (!frame) {
m_threadRunning.store(false);
return;
}
AVPacket pkt{};
while (m_threadRunning.load(std::memory_order_acquire) &&
m_generation.load(std::memory_order_acquire) == generation) {
int ret = av_read_frame(m_fmt, &pkt);
if (ret == AVERROR_EOF) {
if (avformat_seek_file(
m_fmt, m_streamIndex, INT64_MIN, 0, INT64_MAX, 0) < 0) {
qCWarning(videoLog, "VideoPlayer: loop seek failed; stopping");
break;
}
avcodec_flush_buffers(m_codec);
continue;
}
if (ret < 0) {
qCWarning(videoLog, "VideoPlayer: read error; stopping");
break;
}
if (pkt.stream_index != m_streamIndex) {
av_packet_unref(&pkt);
continue;
}
if (avcodec_send_packet(m_codec, &pkt) < 0) {
av_packet_unref(&pkt);
continue;
}
av_packet_unref(&pkt);
while (m_threadRunning.load(std::memory_order_acquire) &&
m_generation.load(std::memory_order_acquire) == generation &&
avcodec_receive_frame(m_codec, frame) == 0) {
bool wasEmpty = false;
{
std::lock_guard<std::mutex> lock(m_mtx);
if (!m_pending) m_pending = av_frame_alloc();
av_frame_unref(m_pending);
if (av_frame_ref(m_pending, frame) >= 0) {
wasEmpty = !m_haveFrame;
m_haveFrame = true;
}
}
if (wasEmpty && m_readyCb) m_readyCb();
if (m_frameCb) m_frameCb();
av_frame_unref(frame);
}
std::this_thread::sleep_for(std::chrono::microseconds(intervalUs));
}
av_frame_free(&frame);
{
CudaCtx ctxGuard(m_cuCtx);
closeDecoder();
}
m_threadRunning.store(false);
}
bool NvidiaPlayer::start(const QString& path) {
if (!ensureCudaContext()) return false;
const uint32_t generation =
m_generation.fetch_add(1, std::memory_order_acq_rel) + 1;
m_threadRunning.store(true);
std::thread previous = std::move(m_thread);
m_thread = std::thread(
[this, path, generation, previous = std::move(previous)]() mutable {
if (previous.joinable()) previous.join();
closeDecoder();
decodeLoop(path, generation);
});
return true;
}
void NvidiaPlayer::stop() {
m_generation.fetch_add(1, std::memory_order_acq_rel);
m_threadRunning.store(false);
if (m_thread.joinable()) m_thread.join();
std::lock_guard<std::mutex> lock(m_mtx);
CudaCtx ctxGuard(m_cuCtx);
closeDecoder();
if (m_pending) {
av_frame_unref(m_pending);
m_pending = nullptr;
}
if (m_displayed) {
av_frame_unref(m_displayed);
m_displayed = nullptr;
}
m_haveFrame = false;
m_size = QSize();
}
void NvidiaPlayer::deleteTexture() {
std::lock_guard<std::mutex> lock(m_mtx);
CUcontext prev = nullptr;
if (m_cuCtx) cuCtxPushCurrent(m_cuCtx);
if (m_resY) {
cuGraphicsUnregisterResource(m_resY);
m_resY = nullptr;
}
if (m_resUV) {
cuGraphicsUnregisterResource(m_resUV);
m_resUV = nullptr;
}
if (m_texY) {
glDeleteTextures(1, &m_texY);
m_texY = 0;
}
if (m_texUV) {
glDeleteTextures(1, &m_texUV);
m_texUV = 0;
}
if (m_cuCtx) cuCtxPopCurrent(&prev);
m_texSize = QSize();
}
bool NvidiaPlayer::ensureCudaContext() {
if (m_cuCtx) return true;
if (cuInit(0) != CUDA_SUCCESS) {
qCWarning(videoLog, "VideoPlayer: cuInit failed");
return false;
}
CUdevice dev = 0;
if (cuDeviceGet(&dev, 0) != CUDA_SUCCESS) {
qCWarning(videoLog, "VideoPlayer: cuDeviceGet failed");
return false;
}
if (cuCtxCreate_v4(&m_cuCtx, nullptr, 0, dev) != CUDA_SUCCESS) {
m_cuCtx = nullptr;
qCWarning(videoLog, "VideoPlayer: cuCtxCreate failed");
return false;
}
CUcontext popped = nullptr;
cuCtxPopCurrent(&popped);
return true;
}
bool NvidiaPlayer::ensureTextures(unsigned w, unsigned h, int compBytes) {
if (!ensureCudaContext()) return false;
CUcontext prev = nullptr;
cuCtxPushCurrent(m_cuCtx);
const QSize sz = QSize(int(w), int(h));
if (m_texSize == sz && m_texComp == compBytes && m_texY != 0) {
cuCtxPopCurrent(&prev);
return true;
}
if (m_resY) cuGraphicsUnregisterResource(m_resY);
if (m_resUV) cuGraphicsUnregisterResource(m_resUV);
m_resY = m_resUV = nullptr;
if (m_texY) glDeleteTextures(1, &m_texY);
if (m_texUV) glDeleteTextures(1, &m_texUV);
m_texY = m_texUV = 0;
const bool wide = compBytes == 2;
auto makeTex =
[&](GLuint& tex, GLint internal, GLenum fmt, unsigned tw, unsigned th) {
glGenTextures(1, &tex);
glBindTexture(GL_TEXTURE_2D, tex);
glTexImage2D(
GL_TEXTURE_2D,
0,
internal,
GLint(tw),
GLint(th),
0,
fmt,
wide ? GL_UNSIGNED_SHORT : GL_UNSIGNED_BYTE,
nullptr);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
};
makeTex(m_texY, wide ? GL_R16 : GL_R8, GL_RED, w, h);
makeTex(m_texUV, wide ? GL_RG16 : GL_RG8, GL_RG, w / 2, h / 2);
CUresult r = cuGraphicsGLRegisterImage(
&m_resY,
m_texY,
GL_TEXTURE_2D,
CU_GRAPHICS_REGISTER_FLAGS_WRITE_DISCARD);
if (r != CUDA_SUCCESS) {
const char* es = nullptr;
cuGetErrorString(r, &es);
qCWarning(
videoLog,
"VideoPlayer: register luma tex failed (%s)",
es ? es : "?");
m_texSize = QSize();
cuCtxPopCurrent(&prev);
return false;
}
r = cuGraphicsGLRegisterImage(
&m_resUV,
m_texUV,
GL_TEXTURE_2D,
CU_GRAPHICS_REGISTER_FLAGS_WRITE_DISCARD);
if (r != CUDA_SUCCESS) {
qCWarning(videoLog, "VideoPlayer: register chroma tex failed");
cuGraphicsUnregisterResource(m_resY);
m_resY = nullptr;
m_texSize = QSize();
cuCtxPopCurrent(&prev);
return false;
}
m_texSize = sz;
m_texComp = compBytes;
cuCtxPopCurrent(&prev);
return true;
}
bool NvidiaPlayer::importCurrentFrame() {
AVFrame* frame = nullptr;
{
std::lock_guard<std::mutex> lock(m_mtx);
if (!m_pending || m_pending->format == AV_PIX_FMT_NONE) return false;
frame = m_pending;
m_pending = nullptr;
}
if (frame->format != AV_PIX_FMT_CUDA || !frame->hw_frames_ctx) {
static int logged = 0;
if (logged++ < 3)
qCWarning(
videoLog,
"VideoPlayer: frame not on GPU (fmt=%s hw_frames=%d)",
av_get_pix_fmt_name(AVPixelFormat(frame->format)),
frame->hw_frames_ctx != nullptr);
av_frame_unref(frame);
return false;
}
const AVHWFramesContext* hf =
reinterpret_cast<const AVHWFramesContext*>(frame->hw_frames_ctx->data);
const AVPixelFormat sw = hf->sw_format;
const bool tenBit = sw == AV_PIX_FMT_P010LE || sw == AV_PIX_FMT_P010BE ||
sw == AV_PIX_FMT_P012LE || sw == AV_PIX_FMT_P016LE;
const int compBytes = tenBit ? 2 : 1;
const size_t compSize = size_t(compBytes);
const unsigned w = unsigned(frame->width);
const unsigned h = unsigned(frame->height);
if (w == 0 || h == 0) {
av_frame_unref(frame);
return false;
}
size_t pitchY = size_t(frame->linesize[0]);
size_t pitchUV = frame->linesize[1] > 0 ? size_t(frame->linesize[1])
: pitchY;
if (pitchY == 0) pitchY = size_t(w) * compSize;
switch (frame->colorspace) {
case AVCOL_SPC_BT470BG:
case AVCOL_SPC_SMPTE170M:
m_matrix = 0;
break;
case AVCOL_SPC_BT709:
m_matrix = 1;
break;
default:
m_matrix = h >= 720 ? 1 : 0;
break;
}
m_range = frame->color_range == AVCOL_RANGE_JPEG ? 1 : 0;
m_size = QSize(int(w), int(h));
if (!ensureTextures(w, h, compBytes)) {
av_frame_unref(frame);
return false;
}
CUcontext prev = nullptr;
cuCtxPushCurrent(m_cuCtx);
CUgraphicsResource res[2] = {m_resY, m_resUV};
CUresult r = cuGraphicsMapResources(2, res, nullptr);
if (r != CUDA_SUCCESS) {
cuCtxPopCurrent(&prev);
av_frame_unref(frame);
qCWarning(videoLog, "VideoPlayer: cuGraphicsMapResources failed");
return false;
}
CUarray arrY = nullptr, arrUV = nullptr;
cuGraphicsSubResourceGetMappedArray(&arrY, m_resY, 0, 0);
cuGraphicsSubResourceGetMappedArray(&arrUV, m_resUV, 0, 0);
const size_t dstPitch = size_t(w) * compSize;
CUDA_MEMCPY2D mc{};
mc.srcMemoryType = CU_MEMORYTYPE_DEVICE;
mc.dstMemoryType = CU_MEMORYTYPE_ARRAY;
mc.srcDevice = CUdeviceptr(frame->data[0]);
mc.srcPitch = pitchY;
mc.dstArray = arrY;
mc.dstPitch = dstPitch;
mc.WidthInBytes = std::min(pitchY, dstPitch);
mc.Height = h;
r = cuMemcpy2D(&mc);
if (r == CUDA_SUCCESS) {
mc.srcDevice = CUdeviceptr(frame->data[1]);
mc.srcPitch = pitchUV;
mc.dstArray = arrUV;
mc.dstPitch = dstPitch;
mc.WidthInBytes = std::min(pitchUV, dstPitch);
mc.Height = h / 2;
r = cuMemcpy2D(&mc);
}
cuGraphicsUnmapResources(2, res, nullptr);
cuCtxPopCurrent(&prev);
if (r != CUDA_SUCCESS) {
av_frame_unref(frame);
qCWarning(videoLog, "VideoPlayer: cuMemcpy2D failed");
return false;
}
{
std::lock_guard<std::mutex> lock(m_mtx);
if (m_displayed) av_frame_unref(m_displayed);
m_displayed = frame;
}
return true;
}
} // namespace ZShell::wallpaper
#endif
+78
View File
@@ -0,0 +1,78 @@
#pragma once
#ifdef ZHAVE_VIDEO
#include "videodecoder.hpp"
#include <EGL/egl.h>
#include <cuda.h>
#include <atomic>
#include <mutex>
#include <thread>
#include <QSize>
#include <QString>
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/buffer.h>
#include <libavutil/frame.h>
#include <libavutil/hwcontext.h>
#include <libavutil/hwcontext_cuda.h>
#include <libavutil/pixfmt.h>
#include <libavutil/pixdesc.h>
}
namespace ZShell::wallpaper {
class NvidiaPlayer : public VideoDecoder {
public:
explicit NvidiaPlayer(EGLDisplay display);
~NvidiaPlayer() override;
[[nodiscard]] static bool available();
bool start(const QString& path) override;
void stop() override;
bool importCurrentFrame() override;
void deleteTexture() override;
[[nodiscard]] const char* backendName() const override { return "nvidia"; }
private:
void decodeLoop(const QString& path, uint32_t generation);
bool openDecoder(const QString& path);
void closeDecoder();
bool ensureTextures(unsigned w, unsigned h, int compBytes);
bool ensureCudaContext();
std::thread m_thread;
std::atomic<bool> m_threadRunning{false};
std::atomic<uint32_t> m_generation{0};
mutable std::mutex m_mtx;
AVFrame* m_pending = nullptr;
bool m_haveFrame = false;
AVFrame* m_displayed = nullptr;
AVFormatContext* m_fmt = nullptr;
AVCodecContext* m_codec = nullptr;
AVBufferRef* m_hwDeviceCtx = nullptr;
int m_streamIndex = -1;
CUcontext m_cuCtx = nullptr;
CUgraphicsResource m_resY = nullptr;
CUgraphicsResource m_resUV = nullptr;
QSize m_texSize;
int m_texComp = 1;
};
} // namespace ZShell::wallpaper
#endif // ZHAVE_VIDEO

Some files were not shown because too many files have changed in this diff Show More