Compare commits
33
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
789305286a | ||
|
|
f8101b2807 | ||
|
|
017d7073d2 | ||
|
|
8271e5d013 | ||
|
|
566b71d386 | ||
|
|
c5608dc9b9 | ||
|
|
78a9577209 | ||
|
|
90184bff58 | ||
|
|
abdde2450d | ||
|
|
fd05b3d4c4 | ||
|
|
067291f478 | ||
|
|
395d1dad3d | ||
|
|
fe0ce991da | ||
|
|
858e4e5b12 | ||
|
|
ed7e977eb5 | ||
|
|
4252812e82 | ||
|
|
a893e6cbe6 | ||
|
|
3792e9d73f | ||
|
|
906b15b092 | ||
|
|
811ad6b7f4 | ||
|
|
76f5a23c2a | ||
|
|
c0744b29b6 | ||
|
|
5896256372 | ||
|
|
118b8f482a | ||
|
|
b58b4fdc9c | ||
|
|
a56afd7fac | ||
|
|
b0cb43f75c | ||
|
|
e655ec5bc7 | ||
|
|
dc0f222ace | ||
|
|
de19fd6ac4 | ||
|
|
9edbcbd610 | ||
|
|
cd44d27b74 | ||
|
|
0d0faecb1c |
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|||||||
@@ -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,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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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] = {};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
@@ -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)
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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
|
||||||
@@ -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
Reference in New Issue
Block a user