Compare commits
34
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11e7aac195 | ||
|
|
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
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
indent: tabs
|
||||||
|
list_expansion: favour-expansion
|
||||||
|
definitions: [Plugins/cmake/qml-module.cmake, Plugins/gersemi/qt-add-shaders.cmake]
|
||||||
@@ -51,7 +51,7 @@ CustomRect {
|
|||||||
if (internalChecked)
|
if (internalChecked)
|
||||||
return checkedRadius;
|
return checkedRadius;
|
||||||
if (isRound)
|
if (isRound)
|
||||||
return implicitHeight / 2 * Math.min(1, Tokens.rounding.scale);
|
return (height ?? implicitHeight) / 2 * Math.min(1, Tokens.rounding.scale);
|
||||||
return defaultRadius;
|
return defaultRadius;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound
|
|||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Shapes
|
import QtQuick.Shapes
|
||||||
|
import ZShell
|
||||||
import ZShell.Components
|
import ZShell.Components
|
||||||
import ZShell.Config
|
import ZShell.Config
|
||||||
import qs.Services
|
import qs.Services
|
||||||
@@ -9,21 +10,50 @@ import qs.Services
|
|||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
readonly property real arcRadius: (size - padding - strokeWidth * (1 + waveAmplitude * 2)) / 2
|
// Ring geometry, quantized in the item's own device pixel space and never
|
||||||
|
// against its position: the parent layout places the item on the device
|
||||||
|
// grid, so these values cannot change when the item moves (no jitter), and
|
||||||
|
// the ring stays crisp wherever it lands.
|
||||||
|
// Device-pixel center of the ring: an even stroke width wants a pixel
|
||||||
|
// boundary, an odd one a pixel center. Floored, matching
|
||||||
|
// WavyLineRenderer::snapCenterDev, so the wave and this background arc
|
||||||
|
// always land on the same center even when the box's device size is odd.
|
||||||
|
readonly property real centerDev: Math.floor(
|
||||||
|
sizeDev / 2 - strokeParity / 2
|
||||||
|
) + strokeParity / 2
|
||||||
|
readonly property real centerX: centerDev / dpr
|
||||||
|
readonly property real centerY: centerDev / dpr
|
||||||
|
// Whole device pixels, and one pixel of slack on the outer edge: the ring's
|
||||||
|
// antialias feather needs somewhere to fade, otherwise it is cut flat at
|
||||||
|
// the top/left/right/bottom of the item rect.
|
||||||
|
readonly property real arcRadius: Math.max(
|
||||||
|
1,
|
||||||
|
Math.floor(
|
||||||
|
(sizeDev - Math.round(padding * dpr) - strokeDev * (1 + waveAmplitude * 2) - 2) / 2
|
||||||
|
)
|
||||||
|
) / dpr
|
||||||
property color bgColor: Colors.palette.m3secondaryContainer
|
property color bgColor: Colors.palette.m3secondaryContainer
|
||||||
property real clampedVal: Math.max(1 / 360, Math.min(1, isNaN(value) ? 0 : value))
|
property real clampedVal: Math.max(1 / 360, Math.min(1, isNaN(value) ? 0 : value))
|
||||||
readonly property real dotAngleRad: (startAngle + sweepAngle - gapAngle * (sweepAngle < 360 ? 0 : 1)) * Math.PI / 180
|
readonly property real dotAngleRad: (startAngle + sweepAngle - gapAngle * (sweepAngle < 360 ? 0 : 1)) * Math.PI / 180
|
||||||
|
readonly property real dpr: ZUtils.dprFor(root)
|
||||||
property color fgColor: Colors.palette.m3primary
|
property color fgColor: Colors.palette.m3primary
|
||||||
readonly property real gapAngle: ((spacing + strokeWidth) / (arcRadius || 1)) * (180 / Math.PI)
|
readonly property real gapAngle: ((spacing + strokeLogical) / (arcRadius || 1)) * (180 / Math.PI)
|
||||||
property alias hasEndIndicator: dot.active
|
property alias hasEndIndicator: dot.active
|
||||||
property real implicitSize
|
property real implicitSize
|
||||||
property int padding: 0
|
property int padding: 0
|
||||||
readonly property real size: Math.min(width, height)
|
// The item's own size in whole device pixels. Never adjusted upwards: the
|
||||||
|
// texture is exactly this wide, so ring values derived from it cannot spill
|
||||||
|
// past the item's bounds.
|
||||||
|
readonly property real size: sizeDev / dpr
|
||||||
|
readonly property real sizeDev: Math.round(Math.min(width, height) * dpr)
|
||||||
property int spacing: Tokens.spacing.small
|
property int spacing: Tokens.spacing.small
|
||||||
|
readonly property real strokeDev: Math.max(1, Math.round(strokeWidth * dpr))
|
||||||
|
readonly property real strokeLogical: strokeDev / dpr
|
||||||
|
readonly property real strokeParity: strokeDev % 2
|
||||||
property int startAngle: -90
|
property int startAngle: -90
|
||||||
property int strokeWidth: Tokens.padding.extraSmall
|
property int strokeWidth: Tokens.padding.extraSmall
|
||||||
property int sweepAngle: 360
|
property int sweepAngle: 360
|
||||||
readonly property real thickness: strokeWidth * (1 + waveAmplitude) * 2
|
readonly property real thickness: strokeLogical * (1 + waveAmplitude) * 2
|
||||||
property real value
|
property real value
|
||||||
property alias waveAmplitude: wave.amplitudeMultiplier
|
property alias waveAmplitude: wave.amplitudeMultiplier
|
||||||
property alias waveDuration: waveProgAnim.duration
|
property alias waveDuration: waveProgAnim.duration
|
||||||
@@ -43,7 +73,7 @@ Item {
|
|||||||
capStyle: ShapePath.RoundCap
|
capStyle: ShapePath.RoundCap
|
||||||
fillColor: "transparent"
|
fillColor: "transparent"
|
||||||
strokeColor: root.bgColor
|
strokeColor: root.bgColor
|
||||||
strokeWidth: Math.min(1, remainingArc.sweepAngle) * root.strokeWidth
|
strokeWidth: Math.max(1, Math.round(Math.min(1, remainingArc.sweepAngle) * root.strokeDev)) / root.dpr
|
||||||
|
|
||||||
Behavior on strokeColor {
|
Behavior on strokeColor {
|
||||||
CAnim {
|
CAnim {
|
||||||
@@ -53,8 +83,8 @@ Item {
|
|||||||
PathAngleArc {
|
PathAngleArc {
|
||||||
id: remainingArc
|
id: remainingArc
|
||||||
|
|
||||||
centerX: root.size / 2
|
centerX: root.centerX
|
||||||
centerY: root.size / 2
|
centerY: root.centerY
|
||||||
radiusX: root.arcRadius
|
radiusX: root.arcRadius
|
||||||
radiusY: root.arcRadius
|
radiusY: root.arcRadius
|
||||||
startAngle: root.startAngle + root.clampedVal * root.sweepAngle + root.gapAngle
|
startAngle: root.startAngle + root.clampedVal * root.sweepAngle + root.gapAngle
|
||||||
@@ -68,11 +98,12 @@ Item {
|
|||||||
|
|
||||||
amplitudeMultiplier: root.wavy ? 0.5 : 0
|
amplitudeMultiplier: root.wavy ? 0.5 : 0
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: -lineWidth * amplitudeMultiplier
|
anchors.margins: -Math.round(lineWidth * amplitudeMultiplier * root.dpr) / root.dpr
|
||||||
color: root.fgColor
|
color: root.fgColor
|
||||||
frequency: 8
|
frequency: 8
|
||||||
fullAngle: root.sweepAngle
|
fullAngle: root.sweepAngle
|
||||||
lineWidth: root.strokeWidth
|
lineWidth: root.strokeWidth
|
||||||
|
objectName: "wave"
|
||||||
pathType: WavyLine.Arc
|
pathType: WavyLine.Arc
|
||||||
radius: root.arcRadius
|
radius: root.arcRadius
|
||||||
startAngle: root.startAngle
|
startAngle: root.startAngle
|
||||||
@@ -103,8 +134,11 @@ Item {
|
|||||||
Loader {
|
Loader {
|
||||||
id: dot
|
id: dot
|
||||||
|
|
||||||
x: root.size / 2 + root.arcRadius * Math.cos(root.dotAngleRad) - width / 2
|
// Rounded in local device pixels: the dot's own size is a whole even
|
||||||
y: root.size / 2 + root.arcRadius * Math.sin(root.dotAngleRad) - height / 2
|
// device count, so an integral local position puts its center on the grid
|
||||||
|
// whatever the ring's angle, and moving the gauge moves the dot with it.
|
||||||
|
x: Math.round((root.centerX + root.arcRadius * Math.cos(root.dotAngleRad) - width / 2) * root.dpr) / root.dpr
|
||||||
|
y: Math.round((root.centerY + root.arcRadius * Math.sin(root.dotAngleRad) - height / 2) * root.dpr) / root.dpr
|
||||||
|
|
||||||
sourceComponent: CustomRect {
|
sourceComponent: CustomRect {
|
||||||
color: root.fgColor
|
color: root.fgColor
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -96,6 +96,8 @@ ProgressBar {
|
|||||||
|
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
implicitWidth: parent.width - wave.implicitWidth - Tokens.spacing.extraSmall
|
implicitWidth: parent.width - wave.implicitWidth - Tokens.spacing.extraSmall
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
}
|
}
|
||||||
|
|
||||||
Line {
|
Line {
|
||||||
@@ -197,7 +199,6 @@ ProgressBar {
|
|||||||
component Line: CustomRect {
|
component Line: CustomRect {
|
||||||
property point bounds
|
property point bounds
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
color: root.bgColor
|
color: root.bgColor
|
||||||
implicitHeight: parent.height
|
implicitHeight: parent.height
|
||||||
implicitWidth: bounds.y - bounds.x
|
implicitWidth: bounds.y - bounds.x
|
||||||
|
|||||||
@@ -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,653 @@
|
|||||||
|
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));
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,6 +55,21 @@ Item {
|
|||||||
return ZUtils.snap(v, canvas);
|
return ZUtils.snap(v, canvas);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A value is plottable only if it is a real number. Everything else -
|
||||||
|
// null, undefined, NaN, an unparsable string from the API - is a hole, so
|
||||||
|
// the series splits there. Non-finite coordinates must never reach the
|
||||||
|
// path: one NaN lineTo makes the canvas ignore the rest of the curve, and
|
||||||
|
// the fill then runs from whatever y it last had straight down.
|
||||||
|
function numeric(v) {
|
||||||
|
if (typeof v === "number")
|
||||||
|
return Number.isFinite(v) ? v : null;
|
||||||
|
if (typeof v === "string") {
|
||||||
|
const n = Number(v);
|
||||||
|
return Number.isFinite(n) ? n : null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function strokeCurve(ctx, pts) {
|
function strokeCurve(ctx, pts) {
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(pts[0].x, pts[0].y);
|
ctx.moveTo(pts[0].x, pts[0].y);
|
||||||
@@ -230,7 +245,8 @@ Item {
|
|||||||
if (loAuto || hiAuto) {
|
if (loAuto || hiAuto) {
|
||||||
let dmin = Infinity;
|
let dmin = Infinity;
|
||||||
let dmax = -Infinity;
|
let dmax = -Infinity;
|
||||||
for (const v of root.values) {
|
for (const value of root.values) {
|
||||||
|
const v = numeric(value);
|
||||||
if (v == null)
|
if (v == null)
|
||||||
continue;
|
continue;
|
||||||
if (v < dmin)
|
if (v < dmin)
|
||||||
@@ -272,7 +288,7 @@ Item {
|
|||||||
if (range <= 0 || root.xSteps < 1)
|
if (range <= 0 || root.xSteps < 1)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
let series = root.values;
|
let series = root.values.map(v => numeric(v));
|
||||||
if (root.smoothing > 0 && series.length > 0) {
|
if (root.smoothing > 0 && series.length > 0) {
|
||||||
const radius = Math.round(root.smoothing);
|
const radius = Math.round(root.smoothing);
|
||||||
const raw = series;
|
const raw = series;
|
||||||
|
|||||||
+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 {
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.Notifications
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import ZShell.Components
|
||||||
|
import ZShell.Config
|
||||||
|
import ZShell.Services
|
||||||
|
import qs.Components
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
ButtonRow {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property NotificationRow notif
|
||||||
|
required property color buttonColour
|
||||||
|
|
||||||
|
readonly property color fillColour: notif.urgency === NotificationUrgency.Critical ? Colors.palette.m3secondary : buttonColour
|
||||||
|
readonly property color buttonTextColour: notif.urgency === NotificationUrgency.Critical ? Colors.palette.m3onSecondary : Colors.palette.m3onSurfaceVariant
|
||||||
|
|
||||||
|
function invoke(action: var): void {
|
||||||
|
action.invoke();
|
||||||
|
if (!notif.resident)
|
||||||
|
notif.dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
spacing: Tokens.spacing.extraSmall
|
||||||
|
|
||||||
|
IconButton {
|
||||||
|
isRound: true
|
||||||
|
shapeMorph: true
|
||||||
|
fillWidth: root.notif.actions.length === 0
|
||||||
|
inactiveColor: root.fillColour
|
||||||
|
inactiveOnColor: root.buttonTextColour
|
||||||
|
icon: "close"
|
||||||
|
padding: Tokens.padding.extraSmall
|
||||||
|
onClicked: root.notif.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.notif.actions
|
||||||
|
|
||||||
|
delegate: root.notif.hasActionIcons ? iconAction : textAction
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton {
|
||||||
|
isRound: true
|
||||||
|
shapeMorph: true
|
||||||
|
fillWidth: root.notif.actions.length === 0
|
||||||
|
inactiveColor: root.fillColour
|
||||||
|
inactiveOnColor: root.buttonTextColour
|
||||||
|
icon: copyTimer.running ? "inventory" : "content_copy"
|
||||||
|
padding: Tokens.padding.extraSmall
|
||||||
|
onClicked: {
|
||||||
|
Quickshell.clipboardText = root.notif.body;
|
||||||
|
copyTimer.restart();
|
||||||
|
}
|
||||||
|
label.animate: true
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: copyTimer
|
||||||
|
|
||||||
|
interval: 3000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: textAction
|
||||||
|
|
||||||
|
TextButton {
|
||||||
|
required property var modelData
|
||||||
|
|
||||||
|
isRound: true
|
||||||
|
shapeMorph: true
|
||||||
|
fillWidth: true
|
||||||
|
inactiveColor: root.fillColour
|
||||||
|
inactiveOnColor: root.buttonTextColour
|
||||||
|
text: modelData.text
|
||||||
|
onClicked: root.invoke(modelData)
|
||||||
|
|
||||||
|
label.horizontalAlignment: Text.AlignHCenter
|
||||||
|
label.anchors.left: left
|
||||||
|
label.anchors.right: right
|
||||||
|
label.anchors.verticalCenter: verticalCenter
|
||||||
|
label.anchors.centerIn: undefined
|
||||||
|
label.anchors.margins: Tokens.padding.medium
|
||||||
|
label.elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: iconAction
|
||||||
|
|
||||||
|
ButtonBase {
|
||||||
|
id: button
|
||||||
|
|
||||||
|
required property var modelData
|
||||||
|
|
||||||
|
isRound: true
|
||||||
|
shapeMorph: true
|
||||||
|
fillWidth: true
|
||||||
|
padding: Tokens.padding.extraSmall
|
||||||
|
inactiveColor: root.fillColour
|
||||||
|
inactiveOnColor: root.buttonTextColour
|
||||||
|
implicitWidth: implicitHeight
|
||||||
|
implicitHeight: icon.implicitHeight + padding * 2
|
||||||
|
onClicked: root.invoke(modelData)
|
||||||
|
|
||||||
|
IconImage {
|
||||||
|
id: icon
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
implicitSize: Math.round(Tokens.font.icon.medium.pointSize * 1.3)
|
||||||
|
asynchronous: true
|
||||||
|
source: Quickshell.iconPath(button.modelData.identifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Shapes
|
||||||
|
import Quickshell
|
||||||
|
import ZShell
|
||||||
|
import M3Shapes
|
||||||
|
import ZShell.Config
|
||||||
|
import qs.Components
|
||||||
|
import qs.Helpers
|
||||||
|
import qs.Services
|
||||||
|
import Quickshell.Services.Notifications
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property var owner
|
||||||
|
required property string image
|
||||||
|
required property string icon
|
||||||
|
required property int urgency
|
||||||
|
required property string summary
|
||||||
|
required property color lowFillColour
|
||||||
|
property real progress: 0
|
||||||
|
property string shownImage
|
||||||
|
property string shownIcon
|
||||||
|
|
||||||
|
readonly property color fillColour: urgency === NotificationUrgency.Critical ? Colors.palette.m3primary : urgency === NotificationUrgency.Low ? lowFillColour : Colors.palette.m3secondaryContainer
|
||||||
|
readonly property color iconColour: urgency === NotificationUrgency.Critical ? Colors.palette.m3onPrimary : urgency === NotificationUrgency.Low ? Colors.palette.m3onSurfaceVariant : Colors.palette.m3onSecondaryContainer
|
||||||
|
|
||||||
|
implicitWidth: Config.notifs.sizes.image
|
||||||
|
implicitHeight: Config.notifs.sizes.image
|
||||||
|
|
||||||
|
// Keeping the previously shown url avoids replaying the fade when a row
|
||||||
|
// swaps between its raw source and the cached blob.
|
||||||
|
onImageChanged: {
|
||||||
|
if (image && !owner.isSameImage(shownImage))
|
||||||
|
shownImage = image;
|
||||||
|
}
|
||||||
|
onIconChanged: {
|
||||||
|
if (icon && !owner.isSameAppIcon(shownIcon))
|
||||||
|
shownIcon = icon;
|
||||||
|
}
|
||||||
|
Component.onCompleted: {
|
||||||
|
shownImage = image;
|
||||||
|
shownIcon = icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: imageComp
|
||||||
|
|
||||||
|
FadeImage {
|
||||||
|
source: Qt.resolvedUrl(root.shownImage)
|
||||||
|
width: Config.notifs.sizes.image
|
||||||
|
height: Config.notifs.sizes.image
|
||||||
|
fadeInLargeAnim: Anim.DefaultEffects
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: appIconComp
|
||||||
|
|
||||||
|
FadeIcon {
|
||||||
|
size: Math.round(Config.notifs.sizes.image * 0.6)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: materialIconComp
|
||||||
|
|
||||||
|
MaterialIcon {
|
||||||
|
animate: true
|
||||||
|
text: Icons.getNotifIcon(root.summary, root.urgency)
|
||||||
|
color: root.iconColour
|
||||||
|
fontStyle: Tokens.font.icon.medium
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialShape {
|
||||||
|
id: bgShape
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
shape: contentLoader.sourceComponent === materialIconComp && root.urgency === NotificationUrgency.Critical ? MaterialShape.SoftBurst : MaterialShape.Circle
|
||||||
|
color: Qt.alpha(root.fillColour, 1)
|
||||||
|
opacity: root.fillColour.a
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
CAnim {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {
|
||||||
|
type: Anim.SlowEffects
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: contentLoader
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
anchors.verticalCenterOffset: sourceComponent === materialIconComp ? 1 : 0
|
||||||
|
asynchronous: true
|
||||||
|
sourceComponent: root.image ? imageComp : root.icon ? appIconComp : materialIconComp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
asynchronous: true
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
active: opacity > 0
|
||||||
|
opacity: root.icon && root.image ? 1 : 0
|
||||||
|
|
||||||
|
sourceComponent: CustomRect {
|
||||||
|
implicitWidth: Config.notifs.sizes.badge
|
||||||
|
implicitHeight: Config.notifs.sizes.badge
|
||||||
|
|
||||||
|
color: root.fillColour
|
||||||
|
radius: Tokens.rounding.full
|
||||||
|
|
||||||
|
FadeIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
size: Math.round(Config.notifs.sizes.badge * 0.6)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {
|
||||||
|
type: root.icon && root.image ? Anim.DefaultEffects : Anim.FastEffects
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
asynchronous: true
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.progress > 0
|
||||||
|
|
||||||
|
sourceComponent: Shape {
|
||||||
|
preferredRendererType: Shape.CurveRenderer
|
||||||
|
|
||||||
|
ShapePath {
|
||||||
|
id: progressShape
|
||||||
|
|
||||||
|
property real sweep: root.progress * 360
|
||||||
|
property real spacing: root.urgency === NotificationUrgency.Critical ? Tokens.spacing.extraSmall : 0
|
||||||
|
|
||||||
|
capStyle: ShapePath.RoundCap
|
||||||
|
fillColor: "transparent"
|
||||||
|
strokeWidth: 2
|
||||||
|
strokeColor: Colors.palette.m3primary
|
||||||
|
|
||||||
|
PathPolyline {
|
||||||
|
function pointAt(angle: real): point {
|
||||||
|
const dist = bgShape.distanceAtAngle(angle) + progressShape.strokeWidth / 2 + progressShape.spacing;
|
||||||
|
const rad = angle * Math.PI / 180;
|
||||||
|
return Qt.point(root.width / 2 + dist * Math.sin(rad), root.height / 2 - dist * Math.cos(rad));
|
||||||
|
}
|
||||||
|
|
||||||
|
path: {
|
||||||
|
bgShape.morphProgress; // mapToItem-free dependency: re-run when the shape morphs
|
||||||
|
const points = [];
|
||||||
|
for (let angle = 0; angle < progressShape.sweep; angle += 3)
|
||||||
|
points.push(pointAt(angle));
|
||||||
|
points.push(pointAt(progressShape.sweep));
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on sweep {
|
||||||
|
Anim {
|
||||||
|
easing: Tokens.anim.emphasizedDecel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on spacing {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on strokeColor {
|
||||||
|
CAnim {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
component FadeIcon: FadeImage {
|
||||||
|
id: fadeIcon
|
||||||
|
|
||||||
|
required property int size
|
||||||
|
|
||||||
|
width: size
|
||||||
|
height: size
|
||||||
|
source: Quickshell.iconPath(root.shownIcon)
|
||||||
|
fillMode: Image.PreserveAspectFit
|
||||||
|
fadeInLargeAnim: Anim.DefaultEffects
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.Notifications
|
||||||
|
import ZShell.Components
|
||||||
|
import ZShell.Config
|
||||||
|
import ZShell.Services
|
||||||
|
import qs.Components
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
CustomRect {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property NotificationGroup group
|
||||||
|
required property Flickable container
|
||||||
|
required property bool expanded
|
||||||
|
property bool expandable: true
|
||||||
|
property int baseSurface: 0
|
||||||
|
property int baseLayer: baseSurface
|
||||||
|
|
||||||
|
readonly property list<NotificationRow> notifs: [...group.activeNotifs]
|
||||||
|
readonly property bool single: notifs.length === 1
|
||||||
|
readonly property alias rowPressed: notifList.rowPressed
|
||||||
|
readonly property list<color> surfaces: [Colors.palette.m3surfaceContainer, Colors.palette.m3surfaceContainerHigh, Colors.palette.m3surfaceContainerHighest]
|
||||||
|
readonly property int nonAnimHeight: {
|
||||||
|
const headerHeight = header.implicitHeight + (expanded ? Tokens.spacing.extraSmall : 0);
|
||||||
|
const columnHeight = headerHeight + notifList.layoutHeight;
|
||||||
|
return Math.round(Math.max(Config.notifs.sizes.image, columnHeight) + Tokens.padding.medium * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
signal requestExpand(expand: bool)
|
||||||
|
signal swiped(notif: NotificationRow)
|
||||||
|
signal linkActivated(notif: NotificationRow)
|
||||||
|
|
||||||
|
function surface(offset: int, layer: int): color {
|
||||||
|
return Colors.layer(surfaces[Math.min(baseSurface + offset, surfaces.length - 1)], baseLayer + layer);
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: group.lock(this)
|
||||||
|
|
||||||
|
anchors.left: parent?.left
|
||||||
|
anchors.right: parent?.right
|
||||||
|
implicitHeight: nonAnimHeight
|
||||||
|
|
||||||
|
clip: true
|
||||||
|
radius: Tokens.rounding.large
|
||||||
|
color: group.urgency === NotificationUrgency.Critical && (single || !expanded) ? Colors.palette.m3secondaryContainer : surface(0, 1)
|
||||||
|
|
||||||
|
Behavior on implicitHeight {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
|
||||||
|
SnapLayout {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.margins: Tokens.padding.medium
|
||||||
|
|
||||||
|
objectName: "notif-content"
|
||||||
|
orientation: Qt.Horizontal
|
||||||
|
spacing: Tokens.spacing.medium
|
||||||
|
|
||||||
|
NotifAvatar {
|
||||||
|
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
|
||||||
|
Layout.topMargin: root.expanded && !root.single ? header.implicitHeight + Tokens.spacing.extraSmall + Tokens.padding.extraSmall : 0
|
||||||
|
owner: root.expanded ? root.notifs[0] ?? root.group : root.group
|
||||||
|
image: root.expanded ? root.notifs[0]?.image ?? "" : root.group.image
|
||||||
|
icon: root.expanded ? root.notifs[0]?.appIcon ?? "" : root.group.appIcon
|
||||||
|
urgency: root.expanded ? root.notifs[0]?.urgency ?? NotificationUrgency.Normal : root.group.urgency
|
||||||
|
lowFillColour: root.surface(2, 2)
|
||||||
|
summary: root.notifs[0]?.summary ?? ""
|
||||||
|
progress: (root.notifs[0]?.progress ?? -1) / 100
|
||||||
|
|
||||||
|
Behavior on Layout.topMargin {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: root.expanded ? Tokens.spacing.extraSmall : 0
|
||||||
|
|
||||||
|
Behavior on spacing {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
|
||||||
|
NotifGroupHeader {
|
||||||
|
id: header
|
||||||
|
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
appName: root.group.appName
|
||||||
|
single: root.single
|
||||||
|
timeStr: root.notifs[0]?.timeStr ?? ""
|
||||||
|
count: root.notifs.length
|
||||||
|
urgency: root.group.urgency
|
||||||
|
pillColour: root.surface(1, 2)
|
||||||
|
expandable: root.expandable
|
||||||
|
expanded: root.expanded
|
||||||
|
onToggle: root.requestExpand(!root.expanded)
|
||||||
|
}
|
||||||
|
|
||||||
|
NotifGroupList {
|
||||||
|
id: notifList
|
||||||
|
|
||||||
|
notifs: root.notifs
|
||||||
|
expanded: root.expanded
|
||||||
|
single: root.single
|
||||||
|
headerHeight: header.implicitHeight
|
||||||
|
headerTitleWidth: header.titleWidth
|
||||||
|
expandable: root.expandable
|
||||||
|
container: root.container
|
||||||
|
itemColour: root.surface(1, 2)
|
||||||
|
avatarColour: root.surface(2, 2)
|
||||||
|
buttonColour: root.surface(2, 3)
|
||||||
|
onRequestExpand: expand => root.requestExpand(expand)
|
||||||
|
onSwiped: notif => root.swiped(notif)
|
||||||
|
onLinkActivated: notif => root.linkActivated(notif)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell.Services.Notifications
|
||||||
|
import ZShell.Components
|
||||||
|
import ZShell.Config
|
||||||
|
import qs.Components
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property string appName
|
||||||
|
required property bool single
|
||||||
|
required property string timeStr
|
||||||
|
required property int count
|
||||||
|
required property int urgency
|
||||||
|
required property color pillColour
|
||||||
|
required property bool expandable
|
||||||
|
required property bool expanded
|
||||||
|
readonly property alias titleWidth: appName.width
|
||||||
|
|
||||||
|
readonly property color expandBtnTextColour: urgency === NotificationUrgency.Critical && !single ? Colors.palette.m3onPrimary : Colors.palette.m3onSurfaceVariant
|
||||||
|
|
||||||
|
signal toggle
|
||||||
|
|
||||||
|
implicitHeight: Math.max(appName.implicitHeight, time.implicitHeight, pill.implicitHeight)
|
||||||
|
objectName: "notif-header"
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
id: appName
|
||||||
|
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: time.left
|
||||||
|
anchors.rightMargin: Tokens.spacing.small
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
opacity: root.single && !root.expanded ? 0 : 1
|
||||||
|
text: root.appName
|
||||||
|
color: Colors.palette.m3onSurfaceVariant
|
||||||
|
font: Tokens.font.body.small
|
||||||
|
elide: Text.ElideRight
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {
|
||||||
|
type: Anim.DefaultEffects
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
id: time
|
||||||
|
|
||||||
|
anchors.right: pill.left
|
||||||
|
anchors.rightMargin: Tokens.spacing.small
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
animate: true
|
||||||
|
text: root.timeStr
|
||||||
|
color: {
|
||||||
|
const col = root.urgency === NotificationUrgency.Critical && (root.single || !root.expanded) ? Colors.palette.m3secondary : Colors.palette.m3outline;
|
||||||
|
return root.expanded && !root.single ? Qt.alpha(col, 0) : col;
|
||||||
|
}
|
||||||
|
font: Tokens.font.body.small
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomRect {
|
||||||
|
id: pill
|
||||||
|
|
||||||
|
property real horizontalPadding: root.single ? implicitHeight - expandIcon.implicitWidth : Tokens.padding.large
|
||||||
|
property real countSpan: root.single ? 0 : groupCount.implicitWidth + Tokens.spacing.extraSmall
|
||||||
|
property real iconInset: root.single ? 0 : Tokens.padding.extraSmall / 2
|
||||||
|
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
enabled: root.expandable
|
||||||
|
// StateLayer inherits this through its parent.enabled binding, so a
|
||||||
|
// non-expandable group gets no hover, ripple, or cursor.
|
||||||
|
|
||||||
|
implicitWidth: horizontalPadding + countSpan + (root.expandable ? expandIcon.implicitWidth : 0)
|
||||||
|
implicitHeight: groupCount.implicitHeight + Tokens.padding.extraSmall
|
||||||
|
|
||||||
|
color: {
|
||||||
|
const c = root.urgency === NotificationUrgency.Critical ? Colors.palette.m3primary : root.pillColour;
|
||||||
|
return root.single ? Qt.alpha(c, 0) : c;
|
||||||
|
}
|
||||||
|
radius: Tokens.rounding.full
|
||||||
|
|
||||||
|
StateLayer {
|
||||||
|
color: root.expandBtnTextColour
|
||||||
|
onClicked: root.toggle()
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: pill.horizontalPadding / 2 + pill.iconInset
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
objectName: "notif-expand"
|
||||||
|
|
||||||
|
implicitWidth: groupCount.implicitWidth
|
||||||
|
implicitHeight: groupCount.implicitHeight
|
||||||
|
opacity: root.single ? 0 : 1
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
id: groupCount
|
||||||
|
|
||||||
|
animate: true
|
||||||
|
text: root.count
|
||||||
|
color: root.expandBtnTextColour
|
||||||
|
font: Tokens.font.body.small
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {
|
||||||
|
type: Anim.DefaultEffects
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialIcon {
|
||||||
|
id: expandIcon
|
||||||
|
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: pill.horizontalPadding / 2 - pill.iconInset
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.verticalCenterOffset: root.expanded && !root.single ? -Tokens.padding.extraSmall / 2 : 0
|
||||||
|
|
||||||
|
visible: root.expandable
|
||||||
|
text: "expand_more"
|
||||||
|
color: root.expandBtnTextColour
|
||||||
|
rotation: root.expanded ? 180 : 0
|
||||||
|
|
||||||
|
Behavior on rotation {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on anchors.verticalCenterOffset {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on horizontalPadding {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on countSpan {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on iconInset {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import ZShell.Components
|
||||||
|
import ZShell.Config
|
||||||
|
import ZShell.Services
|
||||||
|
import qs.Components
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
LazyListView {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property list<NotificationRow> notifs
|
||||||
|
required property bool expanded
|
||||||
|
required property bool expandable
|
||||||
|
required property bool single
|
||||||
|
required property Flickable container
|
||||||
|
property real headerHeight
|
||||||
|
property real headerTitleWidth
|
||||||
|
required property color itemColour
|
||||||
|
required property color avatarColour
|
||||||
|
required property color buttonColour
|
||||||
|
readonly property bool rowPressed: pressedRows > 0
|
||||||
|
property int pressedRows
|
||||||
|
|
||||||
|
signal requestExpand(expand: bool)
|
||||||
|
signal swiped(notif: NotificationRow)
|
||||||
|
signal linkActivated(notif: NotificationRow)
|
||||||
|
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
implicitHeight: contentHeight
|
||||||
|
|
||||||
|
spacing: Math.round(Tokens.spacing.extraSmall)
|
||||||
|
asynchronous: true
|
||||||
|
|
||||||
|
readyDelay: 1
|
||||||
|
cacheBuffer: 400
|
||||||
|
removeDuration: Tokens.anim.durations.normal
|
||||||
|
|
||||||
|
useCustomViewport: true
|
||||||
|
viewport: {
|
||||||
|
tWatcher.transform; // mapToItem is not reactive so use this to trigger updates
|
||||||
|
return Qt.rect(0, container.contentY - mapToItem(container.contentItem, 0, 0).y, width, container.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
model: ScriptModel {
|
||||||
|
// Spread, not `as Array`: `notifs` is a list<NotificationRow> property,
|
||||||
|
// and reading it yields a list reference that ScriptModel cannot assign.
|
||||||
|
values: root.expanded ? [...root.notifs] : [...root.notifs].slice(0, Config.notifs.groupPreviewNum);
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Component {
|
||||||
|
NotifSwipeArea {
|
||||||
|
id: notif
|
||||||
|
|
||||||
|
required property int index
|
||||||
|
required property NotificationRow modelData
|
||||||
|
|
||||||
|
Component.onCompleted: modelData.ref(this)
|
||||||
|
Component.onDestruction: {
|
||||||
|
if (pressed)
|
||||||
|
root.pressedRows--;
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyListView.preferredHeight: LazyListView.removing ? 0 : notifInner.nonAnimHeight
|
||||||
|
LazyListView.visibleHeight: LazyListView.removing ? 0 : notifInner.implicitHeight
|
||||||
|
implicitHeight: notifInner.implicitHeight
|
||||||
|
|
||||||
|
opacity: LazyListView.removing || LazyListView.adding ? 0 : 1
|
||||||
|
scale: LazyListView.removing || LazyListView.adding ? 0.7 : 1
|
||||||
|
|
||||||
|
expanded: root.expanded
|
||||||
|
expandable: root.expandable
|
||||||
|
dragExpand: false
|
||||||
|
hoveringLink: !!notifInner.body?.hoveredLink
|
||||||
|
enabled: root.expandable && root.expanded && !LazyListView.removing
|
||||||
|
acceptedButtons: root.single ? Qt.NoButton : Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||||
|
|
||||||
|
onPressedChanged: root.pressedRows += pressed ? 1 : -1
|
||||||
|
onRequestExpand: expand => root.requestExpand(expand)
|
||||||
|
onSwiped: root.swiped(modelData)
|
||||||
|
onDismissed: modelData.dismiss()
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
function onClosedChanged(): void {
|
||||||
|
if (notif.modelData.closed)
|
||||||
|
closeAnim.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
target: notif.modelData
|
||||||
|
}
|
||||||
|
|
||||||
|
ParallelAnimation {
|
||||||
|
id: closeAnim
|
||||||
|
|
||||||
|
Anim {
|
||||||
|
type: Anim.DefaultEffects
|
||||||
|
target: notif
|
||||||
|
property: "opacity"
|
||||||
|
to: 0
|
||||||
|
}
|
||||||
|
Anim {
|
||||||
|
target: notif
|
||||||
|
property: "x"
|
||||||
|
to: notif.x >= 0 ? notif.width : -notif.width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NotifItem {
|
||||||
|
id: notifInner
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
modelData: notif.modelData
|
||||||
|
expanded: root.expanded
|
||||||
|
single: root.single
|
||||||
|
ready: notif.LazyListView.ready
|
||||||
|
headerHeight: root.headerHeight
|
||||||
|
headerTitleWidth: root.headerTitleWidth
|
||||||
|
showAvatar: notif.index > 0
|
||||||
|
expandedColour: root.itemColour
|
||||||
|
avatarColour: root.avatarColour
|
||||||
|
buttonColour: root.buttonColour
|
||||||
|
onLinkActivated: root.linkActivated(notif.modelData)
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on y {
|
||||||
|
enabled: notif.LazyListView.ready
|
||||||
|
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {
|
||||||
|
type: Anim.DefaultEffects
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on scale {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TransformWatcher {
|
||||||
|
id: tWatcher
|
||||||
|
|
||||||
|
a: root.container.contentItem
|
||||||
|
b: root
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,287 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Quickshell.Services.Notifications
|
||||||
|
import ZShell.Components
|
||||||
|
import ZShell.Config
|
||||||
|
import ZShell.Services
|
||||||
|
import qs.Components
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
CustomRect {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property NotificationRow modelData
|
||||||
|
required property bool expanded
|
||||||
|
required property color expandedColour
|
||||||
|
required property color avatarColour
|
||||||
|
required property color buttonColour
|
||||||
|
property bool showAvatar: true
|
||||||
|
property bool single: false
|
||||||
|
property bool ready: true
|
||||||
|
property real headerHeight
|
||||||
|
property real headerTitleWidth
|
||||||
|
|
||||||
|
readonly property bool critical: modelData.urgency === NotificationUrgency.Critical
|
||||||
|
readonly property bool low: modelData.urgency === NotificationUrgency.Low
|
||||||
|
readonly property CustomText body: (expandedContent.item as ExpandedBody)?.body ?? null
|
||||||
|
readonly property real padding: single ? 0 : Tokens.padding.medium
|
||||||
|
readonly property real headerSummaryOffset: -(headerHeight + summaryHeightMetrics.height) / 2
|
||||||
|
readonly property real inset: expanded ? padding : 0
|
||||||
|
readonly property real summaryOffset: single && !expanded ? headerSummaryOffset : inset
|
||||||
|
readonly property real nonAnimHeight: {
|
||||||
|
if (!expanded)
|
||||||
|
return summaryHeightMetrics.height;
|
||||||
|
const contentHeight = summary.implicitHeight + expandedContent.implicitHeight + expandedContent.anchors.topMargin + padding * 2;
|
||||||
|
return single ? contentHeight : Math.max(contentHeight, Config.notifs.sizes.image + Tokens.padding.extraSmall * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
signal linkActivated
|
||||||
|
|
||||||
|
implicitHeight: nonAnimHeight
|
||||||
|
|
||||||
|
radius: Tokens.rounding.medium
|
||||||
|
color: {
|
||||||
|
const c = root.critical ? Colors.palette.m3secondaryContainer : root.expandedColour;
|
||||||
|
return expanded && !single ? c : Qt.alpha(c, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TextMetrics {
|
||||||
|
id: summaryHeightMetrics
|
||||||
|
|
||||||
|
font: dummySummary.font
|
||||||
|
text: " " // Use this height to prevent weird characters from changing the line height
|
||||||
|
}
|
||||||
|
|
||||||
|
TextMetrics {
|
||||||
|
id: summaryMetrics
|
||||||
|
|
||||||
|
font: dummySummary.font
|
||||||
|
text: root.modelData.summary
|
||||||
|
elide: Text.ElideRight
|
||||||
|
elideWidth: root.single ? root.headerTitleWidth : root.width
|
||||||
|
}
|
||||||
|
|
||||||
|
WrappedLoader {
|
||||||
|
id: avatar
|
||||||
|
|
||||||
|
shouldBeActive: root.expanded && root.showAvatar
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.topMargin: Tokens.padding.extraSmall
|
||||||
|
anchors.leftMargin: -(Config.notifs.sizes.image + Tokens.spacing.medium)
|
||||||
|
|
||||||
|
sourceComponent: NotifAvatar {
|
||||||
|
owner: root.modelData
|
||||||
|
image: root.modelData.image
|
||||||
|
icon: root.modelData.appIcon
|
||||||
|
urgency: root.modelData.urgency
|
||||||
|
lowFillColour: root.avatarColour
|
||||||
|
summary: root.modelData.summary
|
||||||
|
progress: root.modelData.progress / 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WrappedLoader {
|
||||||
|
id: compactSummary
|
||||||
|
|
||||||
|
shouldBeActive: !root.expanded
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: root.inset
|
||||||
|
anchors.topMargin: root.summaryOffset
|
||||||
|
|
||||||
|
sourceComponent: CustomText {
|
||||||
|
animate: root.ready
|
||||||
|
text: summaryMetrics.elidedText
|
||||||
|
color: root.critical ? Colors.palette.m3onSecondaryContainer : root.low ? Colors.palette.m3onSurfaceVariant : Colors.palette.m3onSurface
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on anchors.leftMargin {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on anchors.topMargin {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
id: dummySummary
|
||||||
|
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
|
||||||
|
visible: false
|
||||||
|
text: root.modelData.summary
|
||||||
|
}
|
||||||
|
|
||||||
|
WrappedLoader {
|
||||||
|
id: compactBody
|
||||||
|
|
||||||
|
shouldBeActive: !root.expanded
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.leftMargin: root.inset + (root.single ? 0 : dummySummary.width + Tokens.spacing.small)
|
||||||
|
anchors.topMargin: root.single && root.expanded ? summary.implicitHeight + expandedContent.anchors.topMargin : root.inset
|
||||||
|
|
||||||
|
sourceComponent: CustomText {
|
||||||
|
text: root.modelData.body.replace(/\n/g, " ")
|
||||||
|
color: root.critical ? Colors.palette.m3secondary : root.low ? Colors.palette.m3outline : Colors.palette.m3onSurfaceVariant
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on anchors.leftMargin {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on anchors.topMargin {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WrappedLoader {
|
||||||
|
id: summary
|
||||||
|
|
||||||
|
shouldBeActive: root.expanded
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: root.inset
|
||||||
|
anchors.topMargin: root.summaryOffset
|
||||||
|
width: root.width - root.padding * 2 - timeStr.implicitWidth - Tokens.spacing.small
|
||||||
|
|
||||||
|
sourceComponent: CustomText {
|
||||||
|
text: root.modelData.summary
|
||||||
|
color: root.critical ? Colors.palette.m3onSecondaryContainer : root.low ? Colors.palette.m3onSurfaceVariant : Colors.palette.m3onSurface
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on anchors.leftMargin {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on anchors.topMargin {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WrappedLoader {
|
||||||
|
id: timeStr
|
||||||
|
|
||||||
|
shouldBeActive: root.expanded && !root.single
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: root.inset
|
||||||
|
|
||||||
|
sourceComponent: CustomText {
|
||||||
|
animate: true
|
||||||
|
text: root.modelData.timeStr
|
||||||
|
color: root.critical ? Colors.palette.m3secondary : Colors.palette.m3outline
|
||||||
|
font: Tokens.font.body.small
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on anchors.margins {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WrappedLoader {
|
||||||
|
id: expandedContent
|
||||||
|
|
||||||
|
shouldBeActive: root.expanded
|
||||||
|
anchors.top: summary.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: root.inset
|
||||||
|
anchors.topMargin: Tokens.spacing.extraSmall
|
||||||
|
|
||||||
|
sourceComponent: ExpandedBody {}
|
||||||
|
|
||||||
|
Behavior on anchors.margins {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on implicitHeight {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
|
||||||
|
component ExpandedBody: SnapLayout {
|
||||||
|
readonly property alias body: bodyText
|
||||||
|
|
||||||
|
orientation: Qt.Vertical
|
||||||
|
spacing: Tokens.spacing.medium
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
id: bodyText
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
textFormat: Text.MarkdownText
|
||||||
|
text: root.modelData.body.replace(/(.)\n(?!\n)/g, "$1\n\n") || qsTr("No body here! :/")
|
||||||
|
color: root.critical ? Colors.palette.m3secondary : root.low ? Colors.palette.m3outline : Colors.palette.m3onSurfaceVariant
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
|
||||||
|
onLinkActivated: link => {
|
||||||
|
Qt.openUrlExternally(link);
|
||||||
|
root.linkActivated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NotifActions {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
notif: root.modelData
|
||||||
|
buttonColour: root.buttonColour
|
||||||
|
}
|
||||||
|
}
|
||||||
|
component WrappedLoader: Loader {
|
||||||
|
id: comp
|
||||||
|
|
||||||
|
required property bool shouldBeActive
|
||||||
|
|
||||||
|
active: false
|
||||||
|
opacity: 0
|
||||||
|
|
||||||
|
// Makes the loader load on the same frame shouldBeActive becomes true, which ensures size is set
|
||||||
|
states: State {
|
||||||
|
name: "active"
|
||||||
|
when: comp.shouldBeActive
|
||||||
|
|
||||||
|
PropertyChanges {
|
||||||
|
comp.opacity: 1
|
||||||
|
comp.active: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
transitions: [
|
||||||
|
Transition {
|
||||||
|
from: ""
|
||||||
|
to: "active"
|
||||||
|
|
||||||
|
SequentialAnimation {
|
||||||
|
PropertyAction {
|
||||||
|
property: "active"
|
||||||
|
}
|
||||||
|
Anim {
|
||||||
|
type: Anim.DefaultEffects
|
||||||
|
property: "opacity"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Transition {
|
||||||
|
from: "active"
|
||||||
|
to: ""
|
||||||
|
|
||||||
|
SequentialAnimation {
|
||||||
|
Anim {
|
||||||
|
type: Anim.DefaultEffects
|
||||||
|
property: "opacity"
|
||||||
|
}
|
||||||
|
PropertyAction {
|
||||||
|
property: "active"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import ZShell.Config
|
||||||
|
import qs.Components
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property bool expanded
|
||||||
|
property bool expandable: true
|
||||||
|
property bool dragExpand: expandable
|
||||||
|
property bool hoveringLink
|
||||||
|
property int startY
|
||||||
|
property real lastX
|
||||||
|
property real lastMoveTime
|
||||||
|
property real velocity
|
||||||
|
|
||||||
|
signal swiped
|
||||||
|
signal dismissed
|
||||||
|
signal requestExpand(expand: bool)
|
||||||
|
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: hoveringLink ? Qt.PointingHandCursor : pressedButtons === Qt.LeftButton ? Qt.ClosedHandCursor : undefined
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||||
|
preventStealing: dragExpand
|
||||||
|
|
||||||
|
drag.target: pressedButtons === Qt.LeftButton ? this : null
|
||||||
|
drag.axis: Drag.XAxis
|
||||||
|
|
||||||
|
onPressed: event => {
|
||||||
|
startY = event.y;
|
||||||
|
lastX = x;
|
||||||
|
lastMoveTime = Date.now();
|
||||||
|
velocity = 0;
|
||||||
|
if (event.button === Qt.RightButton && expandable)
|
||||||
|
requestExpand(!expanded);
|
||||||
|
else if (event.button === Qt.MiddleButton)
|
||||||
|
dismissed();
|
||||||
|
}
|
||||||
|
onPositionChanged: event => {
|
||||||
|
if (pressedButtons !== Qt.LeftButton)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const dt = now - lastMoveTime;
|
||||||
|
if (dt > 0) {
|
||||||
|
velocity = (velocity + (x - lastX) / dt * 1000) / 2;
|
||||||
|
lastX = x;
|
||||||
|
lastMoveTime = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dragExpand) {
|
||||||
|
const diffY = event.y - startY;
|
||||||
|
if (Math.abs(diffY) > Config.notifs.expandThreshold)
|
||||||
|
requestExpand(diffY > 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onReleased: {
|
||||||
|
const flung = Date.now() - lastMoveTime < 100 && Math.sign(velocity) === Math.sign(x) && Math.abs(velocity) > Config.notifs.clearVelocity;
|
||||||
|
if (!flung && Math.abs(x) < width * Config.notifs.clearThreshold)
|
||||||
|
x = 0;
|
||||||
|
else
|
||||||
|
swiped();
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on x {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
}
|
||||||
+17
-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,11 @@ 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: []
|
readonly property alias groups: store.groups
|
||||||
property bool loaded
|
property alias list: store.rows
|
||||||
readonly property list<Notif> notClosed: list.filter(n => !n.closed)
|
readonly property alias popupGroups: store.popupGroups
|
||||||
readonly property list<Notif> popups: list.filter(n => n.popup)
|
readonly property list<NotificationRow> notClosed: store.rows.filter(n => !n.closed)
|
||||||
property alias server: server
|
readonly property list<NotificationRow> popups: store.rows.filter(n => n.popup)
|
||||||
|
|
||||||
function shouldThrottle(appName: string): bool {
|
function shouldThrottle(appName: string): bool {
|
||||||
if (props.dnd || ShellState.anySidebarOpen())
|
if (props.dnd || ShellState.anySidebarOpen())
|
||||||
@@ -46,33 +43,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 +51,12 @@ Singleton {
|
|||||||
reloadableId: "notifs"
|
reloadableId: "notifs"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NotificationStore {
|
||||||
|
id: store
|
||||||
|
|
||||||
|
path: `${Paths.state}/notifs.sqlite`
|
||||||
|
}
|
||||||
|
|
||||||
NotificationServer {
|
NotificationServer {
|
||||||
id: server
|
id: server
|
||||||
|
|
||||||
@@ -89,40 +65,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 +78,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 +104,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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+11
-5
@@ -74,7 +74,7 @@ Item {
|
|||||||
id: osdWrapper
|
id: osdWrapper
|
||||||
|
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.rightMargin: sidebar.width * (1 - sidebar.offsetScale)
|
anchors.rightMargin: (sidebar.width - 5) * (1 - sidebar.offsetScale)
|
||||||
|
|
||||||
y: ZUtils.snap((parent.height - implicitHeight) / 2, root)
|
y: ZUtils.snap((parent.height - implicitHeight) / 2, root)
|
||||||
|
|
||||||
@@ -86,7 +86,8 @@ Item {
|
|||||||
Osd.Wrapper {
|
Osd.Wrapper {
|
||||||
id: osd
|
id: osd
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.right: parent.right
|
||||||
|
|
||||||
screen: root.screen
|
screen: root.screen
|
||||||
screenState: root.screenState
|
screenState: root.screenState
|
||||||
sidebarOrSessionVisible: sidebar.visible
|
sidebarOrSessionVisible: sidebar.visible
|
||||||
@@ -154,8 +155,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 +197,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
|
||||||
|
|||||||
+28
-9
@@ -2,16 +2,14 @@ pragma ComponentBehavior: Bound
|
|||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Effects
|
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Wayland
|
import Quickshell.Wayland
|
||||||
import Quickshell.Hyprland
|
import Quickshell.Hyprland
|
||||||
import ZShell.Blobs
|
import ZShell.Blobs
|
||||||
|
import ZShell.Config
|
||||||
import qs.Daemons
|
import qs.Daemons
|
||||||
import qs.Components
|
import qs.Components
|
||||||
import qs.Modules.Bar
|
import qs.Modules.Bar
|
||||||
import ZShell.Config
|
|
||||||
import qs.Helpers
|
import qs.Helpers
|
||||||
import qs.Drawers
|
import qs.Drawers
|
||||||
import qs.Services
|
import qs.Services
|
||||||
@@ -56,7 +54,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)
|
||||||
@@ -192,7 +190,7 @@ CustomWindow {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
color: root.surfaceColor
|
color: root.surfaceColor
|
||||||
shadowColor: Qt.alpha(Colors.palette.m3shadow, Math.max(0, root.shadowOpacity))
|
shadowColor: Qt.alpha(Colors.palette.m3shadow, Math.max(0, root.shadowOpacity))
|
||||||
shadowRadius: 18
|
shadowRadius: 10
|
||||||
smoothing: Config.bar.smoothing
|
smoothing: Config.bar.smoothing
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,7 +246,6 @@ CustomWindow {
|
|||||||
|
|
||||||
atRestOverride: panels.osd.offsetScale < 0.99
|
atRestOverride: panels.osd.offsetScale < 0.99
|
||||||
deformAmount: 0.1
|
deformAmount: 0.1
|
||||||
implicitHeight: panels.osd.height
|
|
||||||
implicitWidth: panels.osd.width
|
implicitWidth: panels.osd.width
|
||||||
panel: panels.osdWrapper
|
panel: panels.osdWrapper
|
||||||
radius: 20
|
radius: 20
|
||||||
@@ -260,7 +257,7 @@ CustomWindow {
|
|||||||
|
|
||||||
atRestOverride: panel.implicitHeight > 0
|
atRestOverride: panel.implicitHeight > 0
|
||||||
panel: panels.notifications
|
panel: panels.notifications
|
||||||
radius: Tokens.rounding.large
|
radius: Tokens.rounding.large + Tokens.padding.smaller
|
||||||
}
|
}
|
||||||
|
|
||||||
PanelBg {
|
PanelBg {
|
||||||
@@ -308,7 +305,7 @@ CustomWindow {
|
|||||||
PanelBg {
|
PanelBg {
|
||||||
id: settingsBg
|
id: settingsBg
|
||||||
|
|
||||||
deformAmount: 0.03
|
deformAmount: 0.01
|
||||||
panel: panels.settings
|
panel: panels.settings
|
||||||
radius: Tokens.rounding.largeIncreased + Tokens.padding.small
|
radius: Tokens.rounding.largeIncreased + Tokens.padding.small
|
||||||
}
|
}
|
||||||
@@ -347,12 +344,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 +381,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 +460,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
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ Item {
|
|||||||
readonly property Popout currentPopout: content.children.find(c => c.shouldBeActive) ?? null
|
readonly property Popout currentPopout: content.children.find(c => c.shouldBeActive) ?? null
|
||||||
required property bool horizontal
|
required property bool horizontal
|
||||||
required property PopoutState popouts
|
required property PopoutState popouts
|
||||||
|
required property Wrapper wrapper
|
||||||
|
|
||||||
implicitHeight: (currentPopout?.implicitHeight ?? 0) + Tokens.padding.smaller * 2
|
implicitHeight: (currentPopout?.implicitHeight ?? 0) + Tokens.padding.smaller * 2
|
||||||
implicitWidth: (currentPopout?.implicitWidth ?? 0) + Tokens.padding.smaller * 2
|
implicitWidth: (currentPopout?.implicitWidth ?? 0) + Tokens.padding.smaller * 2
|
||||||
@@ -98,10 +99,10 @@ Item {
|
|||||||
readonly property bool shouldBeActive: root.popouts.currentName === name
|
readonly property bool shouldBeActive: root.popouts.currentName === name
|
||||||
|
|
||||||
active: false
|
active: false
|
||||||
anchors.centerIn: opacity === 1 ? undefined : parent
|
anchors.centerIn: parent
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
anchors.top: opacity === 1 ? parent.top : undefined
|
// anchors.top: opacity === 1 ? parent.top : undefined
|
||||||
anchors.topMargin: Tokens.padding.smaller
|
// anchors.topMargin: Tokens.padding.smaller
|
||||||
opacity: 0
|
opacity: 0
|
||||||
scale: 0.8
|
scale: 0.8
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ StackView {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
readonly property int itemHeight: 30
|
readonly property int itemHeight: 30
|
||||||
readonly property int panelRadius: ((itemHeight / 2) + Tokens.padding.extraSmall) * Tokens.rounding.scale
|
readonly property int panelRadius: ((itemHeight / 2) + Tokens.padding.smaller) * Tokens.rounding.scale
|
||||||
required property PopoutState popouts
|
required property PopoutState popouts
|
||||||
property int rootWidth: 0
|
property int rootWidth: 0
|
||||||
required property QsMenuHandle trayItem
|
required property QsMenuHandle trayItem
|
||||||
@@ -27,20 +27,15 @@ StackView {
|
|||||||
initialItem: SubMenu {
|
initialItem: SubMenu {
|
||||||
handle: root.trayItem
|
handle: root.trayItem
|
||||||
}
|
}
|
||||||
popEnter: NoAnim {
|
popEnter: NoAnim {}
|
||||||
}
|
popExit: NoAnim {}
|
||||||
popExit: NoAnim {
|
pushEnter: NoAnim {}
|
||||||
}
|
pushExit: NoAnim {}
|
||||||
pushEnter: NoAnim {
|
|
||||||
}
|
|
||||||
pushExit: NoAnim {
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
id: subMenuComp
|
id: subMenuComp
|
||||||
|
|
||||||
SubMenu {
|
SubMenu {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
component NoAnim: Transition {
|
component NoAnim: Transition {
|
||||||
@@ -49,8 +44,8 @@ StackView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
component SubMenu: SnapLayout {
|
component SubMenu: SnapLayout {
|
||||||
orientation: Qt.Vertical
|
|
||||||
id: menu
|
id: menu
|
||||||
|
orientation: Qt.Vertical
|
||||||
|
|
||||||
required property QsMenuHandle handle
|
required property QsMenuHandle handle
|
||||||
property bool isSubMenu
|
property bool isSubMenu
|
||||||
@@ -61,12 +56,10 @@ orientation: Qt.Vertical
|
|||||||
spacing: Tokens.spacing.extraSmall
|
spacing: Tokens.spacing.extraSmall
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Behavior on scale {
|
Behavior on scale {
|
||||||
Anim {
|
Anim {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: shown = true
|
Component.onCompleted: shown = true
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -57,6 +58,7 @@ Item {
|
|||||||
shouldBeActive: root.hasCurrent
|
shouldBeActive: root.hasCurrent
|
||||||
|
|
||||||
sourceComponent: Content {
|
sourceComponent: Content {
|
||||||
|
wrapper: root
|
||||||
horizontal: root.horizontal
|
horizontal: root.horizontal
|
||||||
popouts: popoutState
|
popouts: popoutState
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SnapLayout {
|
SnapLayout {
|
||||||
orientation: Qt.Vertical
|
|
||||||
id: layout
|
id: layout
|
||||||
|
orientation: Qt.Vertical
|
||||||
|
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
anchors.margins: Tokens.padding.large
|
anchors.margins: Tokens.padding.large
|
||||||
@@ -77,7 +77,7 @@ orientation: Qt.Vertical
|
|||||||
|
|
||||||
required property string icon
|
required property string icon
|
||||||
|
|
||||||
implicitSize: ZUtils.snap((root.height - layout.anchors.margins * 2 - layout.spacing * 4) / 5, root)
|
implicitSize: (root.height - layout.anchors.margins * 2 - layout.spacing * 4) / 5
|
||||||
|
|
||||||
Behavior on clampedVal {
|
Behavior on clampedVal {
|
||||||
Anim {}
|
Anim {}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ SnapLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SnapLayout {
|
SnapLayout {
|
||||||
orientation: Qt.Vertical
|
orientation: Qt.Vertical
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
Layout.preferredWidth: root.width / 2
|
Layout.preferredWidth: root.width / 2
|
||||||
@@ -71,11 +71,11 @@ orientation: Qt.Vertical
|
|||||||
id: tempGraph
|
id: tempGraph
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
values: root.forecastToday.map(e => e.tempC)
|
values: root.forecastToday.map(e => e?.tempC ?? null)
|
||||||
verticalLineStep: 3
|
verticalLineStep: 3
|
||||||
xSteps: 24
|
xSteps: 24
|
||||||
horizontalLineStep: 10
|
horizontalLineStep: 10
|
||||||
minHorizontalLines: 4
|
minHorizontalLines: 3
|
||||||
yPaddingTop: horizontalLineStep
|
yPaddingTop: horizontalLineStep
|
||||||
yPaddingBottom: horizontalLineStep
|
yPaddingBottom: horizontalLineStep
|
||||||
xLabel: i => root.formatHour(i)
|
xLabel: i => root.formatHour(i)
|
||||||
@@ -95,7 +95,7 @@ orientation: Qt.Vertical
|
|||||||
}
|
}
|
||||||
|
|
||||||
SnapLayout {
|
SnapLayout {
|
||||||
orientation: Qt.Vertical
|
orientation: Qt.Vertical
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
Layout.preferredWidth: root.width / 2
|
Layout.preferredWidth: root.width / 2
|
||||||
@@ -132,7 +132,7 @@ orientation: Qt.Vertical
|
|||||||
yPaddingTop: 20
|
yPaddingTop: 20
|
||||||
verticalLineStep: 3
|
verticalLineStep: 3
|
||||||
lastXLine: false
|
lastXLine: false
|
||||||
horizontalLineStep: 20
|
horizontalLineStep: 30
|
||||||
xLabel: i => root.formatHour(i)
|
xLabel: i => root.formatHour(i)
|
||||||
yLabel: v => v + "%"
|
yLabel: v => v + "%"
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+132
-99
@@ -6,135 +6,168 @@ import QtQuick
|
|||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import ZShell.Components
|
import ZShell.Components
|
||||||
import ZShell.Config
|
import ZShell.Config
|
||||||
|
import ZShell.Services
|
||||||
import qs.Modules
|
import qs.Modules
|
||||||
import qs.Components
|
import qs.Components
|
||||||
|
import qs.Components.Notifs
|
||||||
import qs.Helpers
|
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.notClosed.length > 0 ? qsTr("%1 notification%2").arg(NotifServer.notClosed.length).arg(NotifServer.notClosed.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.notClosed.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 {
|
||||||
|
id: list
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
clip: true
|
||||||
|
orientation: Qt.Vertical
|
||||||
|
spacing: Tokens.spacing.small
|
||||||
|
|
||||||
|
add: Transition {
|
||||||
|
Anim {
|
||||||
|
from: 0
|
||||||
|
property: "opacity"
|
||||||
|
to: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Anim {
|
||||||
|
duration: Tokens.anim.durations.expressiveDefaultSpatial
|
||||||
|
easing: Tokens.anim.expressiveDefaultSpatial
|
||||||
|
from: 0
|
||||||
|
property: "scale"
|
||||||
|
to: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delegate: NotifRow {}
|
||||||
|
displaced: Transition {
|
||||||
|
Anim {
|
||||||
|
properties: "opacity,scale"
|
||||||
|
to: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Anim {
|
||||||
|
duration: Tokens.anim.durations.expressiveDefaultSpatial
|
||||||
|
easing: Tokens.anim.expressiveDefaultSpatial
|
||||||
|
property: "y"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
model: ScriptModel {
|
||||||
|
values: [...NotifServer.groups].filter(g => g.count > 0)
|
||||||
|
}
|
||||||
|
move: Transition {
|
||||||
|
Anim {
|
||||||
|
properties: "opacity,scale"
|
||||||
|
to: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Anim {
|
||||||
|
duration: Tokens.anim.durations.expressiveDefaultSpatial
|
||||||
|
easing: Tokens.anim.expressiveDefaultSpatial
|
||||||
|
property: "y"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
remove: Transition {
|
||||||
|
Anim {
|
||||||
|
property: "opacity"
|
||||||
|
to: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Anim {
|
||||||
|
property: "scale"
|
||||||
|
to: 0.6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
component NotifRow: Item {
|
||||||
|
id: row
|
||||||
|
|
||||||
|
required property NotificationGroup modelData
|
||||||
|
property bool expanded: false
|
||||||
|
|
||||||
|
function dismissAll(): void {
|
||||||
|
for (const n of [...modelData.activeNotifs])
|
||||||
|
n.dismiss();
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomListView {
|
implicitHeight: card.implicitHeight
|
||||||
anchors.fill: parent
|
implicitWidth: list.width
|
||||||
clip: true
|
|
||||||
spacing: Tokens.spacing.small
|
|
||||||
|
|
||||||
add: Transition {
|
NotifSwipeArea {
|
||||||
Anim {
|
id: area
|
||||||
from: 0
|
|
||||||
property: "opacity"
|
|
||||||
to: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Anim {
|
implicitHeight: card.implicitHeight
|
||||||
duration: Tokens.anim.durations.expressiveDefaultSpatial
|
implicitWidth: row.implicitWidth
|
||||||
easing: Tokens.anim.expressiveDefaultSpatial
|
|
||||||
from: 0
|
|
||||||
property: "scale"
|
|
||||||
to: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
delegate: NotifGroup {}
|
|
||||||
displaced: Transition {
|
|
||||||
Anim {
|
|
||||||
properties: "opacity,scale"
|
|
||||||
to: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Anim {
|
expanded: row.expanded
|
||||||
duration: Tokens.anim.durations.expressiveDefaultSpatial
|
onDismissed: row.dismissAll()
|
||||||
easing: Tokens.anim.expressiveDefaultSpatial
|
onRequestExpand: expand => row.expanded = expand
|
||||||
property: "y"
|
onSwiped: row.dismissAll()
|
||||||
}
|
|
||||||
}
|
|
||||||
model: ScriptModel {
|
|
||||||
values: {
|
|
||||||
const list = NotifServer.notClosed.map(n => [n.appName, null]);
|
|
||||||
return [...new Map(list).keys()];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
move: Transition {
|
|
||||||
Anim {
|
|
||||||
properties: "opacity,scale"
|
|
||||||
to: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Anim {
|
Item {
|
||||||
duration: Tokens.anim.durations.expressiveDefaultSpatial
|
anchors.top: parent.top
|
||||||
easing: Tokens.anim.expressiveDefaultSpatial
|
|
||||||
property: "y"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
remove: Transition {
|
|
||||||
Anim {
|
|
||||||
property: "opacity"
|
|
||||||
to: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
Anim {
|
implicitHeight: card.implicitHeight
|
||||||
property: "scale"
|
implicitWidth: row.implicitWidth
|
||||||
to: 0.6
|
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
NotifGroupCard {
|
||||||
|
id: card
|
||||||
|
|
||||||
|
baseLayer: 1
|
||||||
|
container: list
|
||||||
|
expanded: row.expanded
|
||||||
|
group: row.modelData
|
||||||
|
onRequestExpand: expand => row.expanded = expand
|
||||||
|
onSwiped: notif => notif.dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,324 +0,0 @@
|
|||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import Quickshell.Services.Notifications
|
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import ZShell.Components
|
|
||||||
import ZShell.Config
|
|
||||||
import qs.Modules
|
|
||||||
import qs.Components
|
|
||||||
import qs.Helpers
|
|
||||||
import qs.Daemons
|
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
CustomRect {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
readonly property string appIcon: notifs.find(n => n.appIcon.length > 0)?.appIcon ?? ""
|
|
||||||
property bool expanded
|
|
||||||
readonly property string image: notifs.find(n => n.image.length > 0)?.image ?? ""
|
|
||||||
required property string modelData
|
|
||||||
readonly property list<var> notifs: NotifServer.list.filter(notif => notif.appName === modelData)
|
|
||||||
readonly property string urgency: notifs.some(n => n.urgency === NotificationUrgency.Critical) ? "critical" : notifs.some(n => n.urgency === NotificationUrgency.Normal) ? "normal" : "low"
|
|
||||||
|
|
||||||
anchors.left: parent?.left
|
|
||||||
anchors.right: parent?.right
|
|
||||||
clip: true
|
|
||||||
color: root.urgency === "critical" ? Colors.palette.m3secondaryContainer : Colors.layer(Colors.palette.m3surfaceContainerHigh, 2)
|
|
||||||
implicitHeight: content.implicitHeight + Tokens.padding.small * 2
|
|
||||||
radius: Tokens.rounding.medium
|
|
||||||
|
|
||||||
Behavior on implicitHeight {
|
|
||||||
Anim {
|
|
||||||
duration: Tokens.anim.durations.expressiveDefaultSpatial
|
|
||||||
easing: Tokens.anim.expressiveDefaultSpatial
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SnapLayout {
|
|
||||||
id: content
|
|
||||||
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.margins: Tokens.padding.small
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.top: parent.top
|
|
||||||
|
|
||||||
spacing: Tokens.spacing.medium
|
|
||||||
|
|
||||||
Item {
|
|
||||||
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
|
|
||||||
implicitHeight: Config.notifs.sizes.image
|
|
||||||
implicitWidth: Config.notifs.sizes.image
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: imageComp
|
|
||||||
|
|
||||||
Image {
|
|
||||||
asynchronous: true
|
|
||||||
cache: false
|
|
||||||
fillMode: Image.PreserveAspectCrop
|
|
||||||
height: Config.notifs.sizes.image
|
|
||||||
source: Qt.resolvedUrl(root.image)
|
|
||||||
visible: Config.lock.showNotifIcon
|
|
||||||
width: Config.notifs.sizes.image
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: appIconComp
|
|
||||||
|
|
||||||
ColoredIcon {
|
|
||||||
color: root.urgency === "critical" ? Colors.palette.m3onError : root.urgency === "low" ? Colors.palette.m3onSurface : Colors.palette.m3onSecondaryContainer
|
|
||||||
implicitSize: Math.round(Config.notifs.sizes.image * 0.6)
|
|
||||||
layer.enabled: root.appIcon.endsWith("symbolic")
|
|
||||||
source: Quickshell.iconPath(root.appIcon)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: materialIconComp
|
|
||||||
|
|
||||||
MaterialIcon {
|
|
||||||
color: root.urgency === "critical" ? Colors.palette.m3onError : root.urgency === "low" ? Colors.palette.m3onSurface : Colors.palette.m3onSecondaryContainer
|
|
||||||
fontStyle: Tokens.font.icon.large
|
|
||||||
text: Icons.getNotifIcon(root.notifs[0]?.summary, root.urgency)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ClipRect {
|
|
||||||
anchors.fill: parent
|
|
||||||
color: root.urgency === "critical" ? Colors.palette.m3error : root.urgency === "low" ? Colors.layer(Colors.palette.m3surfaceContainerHighest, 3) : Colors.palette.m3secondaryContainer
|
|
||||||
radius: Tokens.rounding.full
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
sourceComponent: root.image ? imageComp : root.appIcon ? appIconComp : materialIconComp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
active: root.appIcon && root.image
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.right: parent.right
|
|
||||||
|
|
||||||
sourceComponent: CustomRect {
|
|
||||||
color: root.urgency === "critical" ? Colors.palette.m3error : root.urgency === "low" ? Colors.palette.m3surfaceContainerHighest : Colors.palette.m3secondaryContainer
|
|
||||||
implicitHeight: Config.notifs.sizes.badge
|
|
||||||
implicitWidth: Config.notifs.sizes.badge
|
|
||||||
radius: Tokens.rounding.full
|
|
||||||
|
|
||||||
ColoredIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
color: root.urgency === "critical" ? Colors.palette.m3onError : root.urgency === "low" ? Colors.palette.m3onSurface : Colors.palette.m3onSecondaryContainer
|
|
||||||
implicitSize: Math.round(Config.notifs.sizes.badge * 0.6)
|
|
||||||
layer.enabled: root.appIcon.endsWith("symbolic")
|
|
||||||
source: Quickshell.iconPath(root.appIcon)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SnapLayout {
|
|
||||||
Layout.bottomMargin: -Tokens.padding.extraSmall / 2 - (root.expanded ? 0 : spacing)
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.topMargin: -Tokens.padding.extraSmall
|
|
||||||
|
|
||||||
spacing: Math.round(Tokens.spacing.small / 2)
|
|
||||||
orientation: Qt.Vertical
|
|
||||||
|
|
||||||
SnapLayout {
|
|
||||||
Layout.bottomMargin: -parent.spacing
|
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
spacing: Tokens.spacing.small
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
color: Colors.palette.m3onSurfaceVariant
|
|
||||||
elide: Text.ElideRight
|
|
||||||
font: Tokens.font.body.small
|
|
||||||
text: root.modelData
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
animate: true
|
|
||||||
color: Colors.palette.m3outline
|
|
||||||
font: Tokens.font.body.small
|
|
||||||
text: root.notifs[0]?.timeStr ?? ""
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomRect {
|
|
||||||
Layout.preferredWidth: root.notifs.length > Config.notifs.groupPreviewNum ? implicitWidth : 0
|
|
||||||
color: root.urgency === "critical" ? Colors.palette.m3error : Colors.layer(Colors.palette.m3surfaceContainerHighest, 2)
|
|
||||||
implicitHeight: groupCount.implicitHeight + Tokens.padding.extraSmall
|
|
||||||
implicitWidth: expandBtn.implicitWidth + Tokens.padding.small * 2
|
|
||||||
opacity: root.notifs.length > Config.notifs.groupPreviewNum ? 1 : 0
|
|
||||||
radius: Tokens.rounding.full
|
|
||||||
|
|
||||||
Behavior on Layout.preferredWidth {
|
|
||||||
Anim {}
|
|
||||||
}
|
|
||||||
Behavior on opacity {
|
|
||||||
Anim {}
|
|
||||||
}
|
|
||||||
|
|
||||||
StateLayer {
|
|
||||||
color: root.urgency === "critical" ? Colors.palette.m3onError : Colors.palette.m3onSurface
|
|
||||||
|
|
||||||
onClicked: {
|
|
||||||
root.expanded = !root.expanded;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SnapLayout {
|
|
||||||
id: expandBtn
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
|
|
||||||
spacing: Tokens.spacing.small / 2
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
id: groupCount
|
|
||||||
|
|
||||||
Layout.leftMargin: Tokens.padding.extraSmall / 2
|
|
||||||
animate: true
|
|
||||||
color: root.urgency === "critical" ? Colors.palette.m3onError : Colors.palette.m3onSurface
|
|
||||||
font: Tokens.font.body.small
|
|
||||||
text: root.notifs.length
|
|
||||||
}
|
|
||||||
|
|
||||||
MaterialIcon {
|
|
||||||
Layout.rightMargin: -Tokens.padding.extraSmall / 2
|
|
||||||
animate: true
|
|
||||||
color: root.urgency === "critical" ? Colors.palette.m3onError : Colors.palette.m3onSurface
|
|
||||||
text: root.expanded ? "expand_less" : "expand_more"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: ScriptModel {
|
|
||||||
values: root.notifs.slice(0, Config.notifs.groupPreviewNum)
|
|
||||||
}
|
|
||||||
|
|
||||||
NotifLine {
|
|
||||||
id: notif
|
|
||||||
|
|
||||||
ParallelAnimation {
|
|
||||||
running: true
|
|
||||||
|
|
||||||
Anim {
|
|
||||||
from: 0
|
|
||||||
property: "opacity"
|
|
||||||
target: notif
|
|
||||||
to: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Anim {
|
|
||||||
from: 0.7
|
|
||||||
property: "scale"
|
|
||||||
target: notif
|
|
||||||
to: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Anim {
|
|
||||||
from: 0
|
|
||||||
property: "preferredHeight"
|
|
||||||
target: notif.Layout
|
|
||||||
to: notif.implicitHeight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ParallelAnimation {
|
|
||||||
running: notif.modelData.closed
|
|
||||||
|
|
||||||
onFinished: notif.modelData.unlock(notif)
|
|
||||||
|
|
||||||
Anim {
|
|
||||||
property: "opacity"
|
|
||||||
target: notif
|
|
||||||
to: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
Anim {
|
|
||||||
property: "scale"
|
|
||||||
target: notif
|
|
||||||
to: 0.7
|
|
||||||
}
|
|
||||||
|
|
||||||
Anim {
|
|
||||||
property: "preferredHeight"
|
|
||||||
target: notif.Layout
|
|
||||||
to: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.preferredHeight: root.expanded ? implicitHeight : 0
|
|
||||||
active: opacity > 0
|
|
||||||
opacity: root.expanded ? 1 : 0
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
Anim {}
|
|
||||||
}
|
|
||||||
sourceComponent: SnapLayout {
|
|
||||||
orientation: Qt.Vertical
|
|
||||||
spacing: Tokens.spacing.extraSmall
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: ScriptModel {
|
|
||||||
values: root.notifs.slice(Config.notifs.groupPreviewNum)
|
|
||||||
}
|
|
||||||
|
|
||||||
NotifLine {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
component NotifLine: CustomText {
|
|
||||||
id: notifLine
|
|
||||||
|
|
||||||
required property NotifServer.Notif modelData
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
color: root.urgency === "critical" ? Colors.palette.m3onSecondaryContainer : Colors.palette.m3onSurface
|
|
||||||
text: {
|
|
||||||
if (!Config.lock.showNotifContent)
|
|
||||||
return "Unlock to view";
|
|
||||||
const summary = modelData.summary.replace(/\n/g, " ");
|
|
||||||
const body = modelData.body.replace(/\n/g, " ");
|
|
||||||
const color = root.urgency === "critical" ? Colors.palette.m3secondary : Colors.palette.m3outline;
|
|
||||||
|
|
||||||
if (metrics.text === metrics.elidedText)
|
|
||||||
return `${summary} <span style='color:${color}'>${body}</span>`;
|
|
||||||
|
|
||||||
const t = metrics.elidedText.length - 3;
|
|
||||||
if (t < summary.length)
|
|
||||||
return `${summary.slice(0, t)}...`;
|
|
||||||
|
|
||||||
return `${summary} <span style='color:${color}'>${body.slice(0, t - summary.length)}...</span>`;
|
|
||||||
}
|
|
||||||
textFormat: Text.MarkdownText
|
|
||||||
|
|
||||||
Component.onCompleted: modelData.lock(this)
|
|
||||||
Component.onDestruction: modelData.unlock(this)
|
|
||||||
|
|
||||||
TextMetrics {
|
|
||||||
id: metrics
|
|
||||||
|
|
||||||
elide: Text.ElideRight
|
|
||||||
elideWidth: notifLine.width
|
|
||||||
font.family: notifLine.font.family
|
|
||||||
font.pointSize: notifLine.font.pointSize
|
|
||||||
text: `${notifLine.modelData.summary} ${notifLine.modelData.body}`.replace(/\n/g, " ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,14 @@ pragma ComponentBehavior: Bound
|
|||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Widgets
|
import Quickshell.Widgets
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import ZShell.Config
|
|
||||||
import ZShell.Components
|
import ZShell.Components
|
||||||
|
import ZShell.Config
|
||||||
|
import ZShell.Services
|
||||||
import qs.Components
|
import qs.Components
|
||||||
|
import qs.Components.Notifs
|
||||||
import qs.Daemons
|
import qs.Daemons
|
||||||
import qs.Helpers
|
import qs.Helpers
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
@@ -20,13 +23,10 @@ Item {
|
|||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
implicitHeight: {
|
implicitHeight: {
|
||||||
const count = list.count;
|
if (list.count === 0)
|
||||||
if (count === 0)
|
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
let height = (count - 1) * Tokens.spacing.small;
|
let height = list.layoutHeight;
|
||||||
for (let i = 0; i < count; i++)
|
|
||||||
height += (list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0;
|
|
||||||
|
|
||||||
if (panels.popouts.hasCurrent && (panels.popouts.currentCenter + (panels.popouts.current?.width / 2)) > panels.notifications.x)
|
if (panels.popouts.hasCurrent && (panels.popouts.currentCenter + (panels.popouts.current?.width / 2)) > panels.notifications.x)
|
||||||
return 0;
|
return 0;
|
||||||
@@ -53,36 +53,55 @@ Item {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: root.padding
|
anchors.margins: root.padding
|
||||||
clip: true
|
clip: true
|
||||||
// radius: Tokens.rounding.large - root.padding
|
|
||||||
|
|
||||||
CustomListView {
|
KineticFlickable {
|
||||||
id: list
|
id: view
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
cacheBuffer: (QsWindow.window as QsWindow)?.screen.height ?? 0
|
|
||||||
orientation: Qt.Vertical
|
|
||||||
spacing: 0
|
|
||||||
|
|
||||||
delegate: NotifWrapper {}
|
contentHeight: list.implicitHeight
|
||||||
displaced: Transition {
|
contentWidth: width
|
||||||
Anim {
|
flickableDirection: Flickable.VerticalFlick
|
||||||
property: "y"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
model: ScriptModel {
|
|
||||||
values: {
|
|
||||||
const popups = NotifServer.popups.filter(n => !n.closed);
|
|
||||||
|
|
||||||
if (!Config.notifs.showInFullscreen) {
|
LazyListView {
|
||||||
if (Hypr.monitorFor(root.panels.screen).activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false)
|
id: list
|
||||||
return [];
|
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
implicitHeight: contentHeight
|
||||||
|
|
||||||
|
spacing: Tokens.spacing.medium
|
||||||
|
readyDelay: 1
|
||||||
|
cacheBuffer: (QsWindow.window as QsWindow)?.screen.height ?? 0
|
||||||
|
removeDuration: Tokens.anim.durations.normal
|
||||||
|
|
||||||
|
useCustomViewport: true
|
||||||
|
viewport: Qt.rect(0, view.contentY, width, view.height)
|
||||||
|
|
||||||
|
model: ScriptModel {
|
||||||
|
values: {
|
||||||
|
if (!Config.notifs.showInFullscreen) {
|
||||||
|
if (Hypr.monitorFor(root.panels.screen).activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false)
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [...NotifServer.popupGroups];
|
||||||
}
|
}
|
||||||
return popups;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
move: Transition {
|
delegate: NotifWrapper {}
|
||||||
|
|
||||||
|
onViewportAdjustNeeded: d => {
|
||||||
|
if (contentYAnim.running)
|
||||||
|
contentYAnim.complete();
|
||||||
|
contentYAnim.to = Math.max(0, view.contentY + d);
|
||||||
|
contentYAnim.start();
|
||||||
|
}
|
||||||
|
|
||||||
Anim {
|
Anim {
|
||||||
property: "y"
|
id: contentYAnim
|
||||||
|
|
||||||
|
target: view
|
||||||
|
property: "contentY"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,74 +114,106 @@ Item {
|
|||||||
component NotifWrapper: Item {
|
component NotifWrapper: Item {
|
||||||
id: wrapper
|
id: wrapper
|
||||||
|
|
||||||
property int idx
|
required property NotificationGroup modelData
|
||||||
required property int index
|
property bool expanded: Config.notifs.openExpanded
|
||||||
required property NotifServer.Notif modelData
|
readonly property bool holding: area.containsMouse || area.pressed || card.rowPressed
|
||||||
readonly property alias nonAnimHeight: notif.nonAnimHeight
|
|
||||||
|
|
||||||
implicitHeight: notif.implicitHeight + (idx === 0 ? 0 : Tokens.spacing.small)
|
function forEachPopup(fn: var): void {
|
||||||
implicitWidth: notif.implicitWidth
|
for (const n of [...card.notifs])
|
||||||
|
fn(n);
|
||||||
ListView.onRemove: removeAnim.start()
|
|
||||||
onIndexChanged: {
|
|
||||||
if (index !== -1)
|
|
||||||
idx = index;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SequentialAnimation {
|
function holdPopups(): void {
|
||||||
id: removeAnim
|
forEachPopup(n => n.holdPopup());
|
||||||
|
|
||||||
PropertyAction {
|
|
||||||
property: "ListView.delayRemove"
|
|
||||||
target: wrapper
|
|
||||||
value: true
|
|
||||||
}
|
|
||||||
|
|
||||||
PropertyAction {
|
|
||||||
property: "enabled"
|
|
||||||
target: wrapper
|
|
||||||
value: false
|
|
||||||
}
|
|
||||||
|
|
||||||
PropertyAction {
|
|
||||||
property: "implicitHeight"
|
|
||||||
target: wrapper
|
|
||||||
value: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
PropertyAction {
|
|
||||||
property: "z"
|
|
||||||
target: wrapper
|
|
||||||
value: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Anim {
|
|
||||||
property: "x"
|
|
||||||
target: notif
|
|
||||||
to: (notif.x >= 0 ? Config.notifs.sizes.width : -Config.notifs.sizes.width) * 2
|
|
||||||
}
|
|
||||||
|
|
||||||
PropertyAction {
|
|
||||||
property: "ListView.delayRemove"
|
|
||||||
target: wrapper
|
|
||||||
value: false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomRect {
|
LazyListView.trackViewport: !expanded && card.nonAnimHeight < card.implicitHeight
|
||||||
|
LazyListView.preferredHeight: LazyListView.removing ? 0 : card.nonAnimHeight
|
||||||
|
LazyListView.visibleHeight: LazyListView.removing ? 0 : card.implicitHeight
|
||||||
|
LazyListView.onRemovingChanged: {
|
||||||
|
if (!wrapper.LazyListView.removing)
|
||||||
|
return;
|
||||||
|
|
||||||
|
slideOut.to = (area.x >= 0 ? root.implicitWidth : -root.implicitWidth) * 2;
|
||||||
|
slideOut.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
implicitWidth: area.implicitWidth
|
||||||
|
implicitHeight: card.implicitHeight
|
||||||
|
|
||||||
|
enabled: !LazyListView.removing
|
||||||
|
z: LazyListView.removing ? 1 : 0
|
||||||
|
|
||||||
|
onHoldingChanged: {
|
||||||
|
if (holding)
|
||||||
|
holdPopups();
|
||||||
|
else
|
||||||
|
forEachPopup(n => n.releasePopup());
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
function onNotifsChanged(): void {
|
||||||
|
if (wrapper.holding)
|
||||||
|
wrapper.holdPopups();
|
||||||
|
}
|
||||||
|
|
||||||
|
target: wrapper.modelData
|
||||||
|
}
|
||||||
|
|
||||||
|
Anim {
|
||||||
|
id: slideOut
|
||||||
|
|
||||||
|
target: area
|
||||||
|
property: "x"
|
||||||
|
duration: Tokens.anim.durations.normal
|
||||||
|
easing: Tokens.anim.emphasized
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
anchors.topMargin: wrapper.idx === 0 ? 0 : 8
|
|
||||||
color: "transparent"
|
|
||||||
implicitHeight: notif.implicitHeight
|
|
||||||
implicitWidth: notif.implicitWidth
|
|
||||||
radius: Tokens.rounding.small
|
|
||||||
|
|
||||||
Notification {
|
implicitWidth: area.implicitWidth
|
||||||
id: notif
|
implicitHeight: card.implicitHeight
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
NotifSwipeArea {
|
||||||
|
id: area
|
||||||
|
|
||||||
implicitWidth: root.implicitWidth - root.padding * 2
|
implicitWidth: root.implicitWidth - root.padding * 2
|
||||||
modelData: wrapper.modelData
|
implicitHeight: card.implicitHeight
|
||||||
|
x: wrapper.LazyListView.adding ? implicitWidth : 0
|
||||||
|
|
||||||
|
expanded: wrapper.expanded
|
||||||
|
|
||||||
|
onRequestExpand: expand => wrapper.expanded = expand
|
||||||
|
onSwiped: wrapper.forEachPopup(n => n.dismissPopup())
|
||||||
|
onDismissed: wrapper.forEachPopup(n => n.dismiss())
|
||||||
|
onClicked: event => {
|
||||||
|
if (!Config.notifs.actionOnClick || event.button !== Qt.LeftButton || card.notifs.length !== 1)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const actions = card.notifs[0].actions;
|
||||||
|
if (actions.length === 1)
|
||||||
|
actions[0].invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
NotifGroupCard {
|
||||||
|
id: card
|
||||||
|
|
||||||
|
group: wrapper.modelData
|
||||||
|
container: view
|
||||||
|
expanded: wrapper.expanded
|
||||||
|
onRequestExpand: expand => wrapper.expanded = expand
|
||||||
|
onSwiped: notif => notif.dismissPopup()
|
||||||
|
onLinkActivated: notif => notif.dismissPopup()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Behavior on y {
|
||||||
|
enabled: wrapper.LazyListView.ready
|
||||||
|
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,510 +0,0 @@
|
|||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Shapes
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Services.Notifications
|
|
||||||
import ZShell.Components
|
|
||||||
import qs.Daemons
|
|
||||||
import qs.Helpers
|
|
||||||
import ZShell.Config
|
|
||||||
import qs.Components
|
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
CustomRect {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool expanded: Config.notifs.openExpanded
|
|
||||||
readonly property bool hasAppIcon: modelData.appIcon.length > 0
|
|
||||||
readonly property bool hasImage: modelData.image.length > 0
|
|
||||||
required property NotifServer.Notif 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
|
|
||||||
|
|
||||||
color: root.modelData.urgency === NotificationUrgency.Critical ? Colors.palette.m3secondaryContainer : Colors.tPalette.m3surfaceContainer
|
|
||||||
implicitHeight: inner.implicitHeight
|
|
||||||
radius: Tokens.rounding.large - Tokens.padding.smaller
|
|
||||||
x: implicitWidth
|
|
||||||
|
|
||||||
Behavior on x {
|
|
||||||
Anim {
|
|
||||||
easing: Tokens.anim.emphasizedDecel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
x = 0;
|
|
||||||
modelData.lock(this);
|
|
||||||
}
|
|
||||||
Component.onDestruction: modelData.unlock(this)
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
property int startY
|
|
||||||
|
|
||||||
acceptedButtons: Qt.LeftButton | Qt.MiddleButton
|
|
||||||
anchors.fill: parent
|
|
||||||
cursorShape: root.expanded && body.hoveredLink ? Qt.PointingHandCursor : pressed ? Qt.ClosedHandCursor : undefined
|
|
||||||
drag.axis: Drag.XAxis
|
|
||||||
drag.target: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
preventStealing: true
|
|
||||||
|
|
||||||
onClicked: event => {
|
|
||||||
if (!Config.notifs.actionOnClick || event.button !== Qt.LeftButton)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const actions = root.modelData.actions;
|
|
||||||
if (actions.length === 1)
|
|
||||||
actions[0].invoke();
|
|
||||||
}
|
|
||||||
onEntered: root.modelData.timer.stop()
|
|
||||||
onExited: {
|
|
||||||
if (!pressed)
|
|
||||||
root.modelData.timer.start();
|
|
||||||
}
|
|
||||||
onPositionChanged: event => {
|
|
||||||
if (pressed) {
|
|
||||||
const diffY = event.y - startY;
|
|
||||||
if (Math.abs(diffY) > Config.notifs.expandThreshold)
|
|
||||||
root.expanded = diffY > 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onPressed: event => {
|
|
||||||
root.modelData.timer.stop();
|
|
||||||
startY = event.y;
|
|
||||||
if (event.button === Qt.MiddleButton)
|
|
||||||
root.modelData.close();
|
|
||||||
}
|
|
||||||
onReleased: event => {
|
|
||||||
if (!containsMouse)
|
|
||||||
root.modelData.timer.start();
|
|
||||||
|
|
||||||
if (Math.abs(root.x) < root.implicitWidth * Config.notifs.clearThreshold)
|
|
||||||
root.x = 0;
|
|
||||||
else
|
|
||||||
root.modelData.popup = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: inner
|
|
||||||
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.margins: Tokens.padding.small
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.top: parent.top
|
|
||||||
implicitHeight: root.nonAnimHeight
|
|
||||||
|
|
||||||
Behavior on implicitHeight {
|
|
||||||
Anim {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: image
|
|
||||||
|
|
||||||
active: root.hasImage
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.top: parent.top
|
|
||||||
asynchronous: true
|
|
||||||
height: Config.notifs.sizes.image
|
|
||||||
visible: root.hasImage || root.hasAppIcon
|
|
||||||
width: Config.notifs.sizes.image
|
|
||||||
|
|
||||||
sourceComponent: ClipRect {
|
|
||||||
color: root.modelData.urgency === NotificationUrgency.Critical ? Colors.palette.m3error : root.modelData.urgency === NotificationUrgency.Low ? Colors.layer(Colors.palette.m3surfaceContainerHighest, 2) : Colors.palette.m3secondaryContainer
|
|
||||||
implicitHeight: Config.notifs.sizes.image
|
|
||||||
implicitWidth: Config.notifs.sizes.image
|
|
||||||
radius: Tokens.rounding.full
|
|
||||||
|
|
||||||
Image {
|
|
||||||
anchors.fill: parent
|
|
||||||
asynchronous: true
|
|
||||||
cache: false
|
|
||||||
fillMode: Image.PreserveAspectCrop
|
|
||||||
source: Qt.resolvedUrl(root.modelData.image)
|
|
||||||
sourceSize: {
|
|
||||||
const size = Config.notifs.sizes.image * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1);
|
|
||||||
return Qt.size(size, size);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: appIcon
|
|
||||||
|
|
||||||
active: root.hasAppIcon || !root.hasImage
|
|
||||||
anchors.bottom: root.hasImage ? image.bottom : undefined
|
|
||||||
anchors.horizontalCenter: root.hasImage ? undefined : image.horizontalCenter
|
|
||||||
anchors.right: root.hasImage ? image.right : undefined
|
|
||||||
anchors.verticalCenter: root.hasImage ? undefined : image.verticalCenter
|
|
||||||
asynchronous: true
|
|
||||||
|
|
||||||
sourceComponent: CustomRect {
|
|
||||||
color: root.modelData.urgency === NotificationUrgency.Critical ? Colors.palette.m3error : root.modelData.urgency === NotificationUrgency.Low ? Colors.layer(Colors.palette.m3surfaceContainerHighest, 2) : Colors.palette.m3secondaryContainer
|
|
||||||
implicitHeight: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image
|
|
||||||
implicitWidth: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image
|
|
||||||
radius: Tokens.rounding.full
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: icon
|
|
||||||
|
|
||||||
active: root.hasAppIcon
|
|
||||||
anchors.centerIn: parent
|
|
||||||
asynchronous: true
|
|
||||||
height: Math.round(parent.width * 0.6)
|
|
||||||
width: Math.round(parent.width * 0.6)
|
|
||||||
|
|
||||||
sourceComponent: ColoredIcon {
|
|
||||||
anchors.fill: parent
|
|
||||||
color: root.modelData.urgency === NotificationUrgency.Critical ? Colors.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? Colors.palette.m3onSurface : Colors.palette.m3onSecondaryContainer
|
|
||||||
layer.enabled: root.modelData.appIcon.endsWith("symbolic")
|
|
||||||
source: Quickshell.iconPath(root.modelData.appIcon)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
active: !root.hasAppIcon
|
|
||||||
anchors.centerIn: parent
|
|
||||||
anchors.verticalCenterOffset: 1
|
|
||||||
asynchronous: true
|
|
||||||
|
|
||||||
sourceComponent: MaterialIcon {
|
|
||||||
color: root.modelData.urgency === NotificationUrgency.Critical ? Colors.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? Colors.palette.m3onSurface : Colors.palette.m3onSecondaryContainer
|
|
||||||
text: Icons.getNotifIcon(root.modelData.summary, root.modelData.urgency)
|
|
||||||
fontStyle: Tokens.font.icon.medium
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Shape {
|
|
||||||
id: progressIndicator
|
|
||||||
|
|
||||||
anchors.centerIn: appIcon
|
|
||||||
height: appIcon.implicitHeight + progressShape.strokeWidth * 2
|
|
||||||
preferredRendererType: Shape.CurveRenderer
|
|
||||||
width: appIcon.implicitWidth + progressShape.strokeWidth * 2
|
|
||||||
|
|
||||||
ShapePath {
|
|
||||||
id: progressShape
|
|
||||||
|
|
||||||
capStyle: ShapePath.RoundCap
|
|
||||||
fillColor: "transparent"
|
|
||||||
strokeColor: Colors.palette.m3primary
|
|
||||||
strokeWidth: 2
|
|
||||||
|
|
||||||
PathAngleArc {
|
|
||||||
id: progressArc
|
|
||||||
|
|
||||||
centerX: progressIndicator.width / 2
|
|
||||||
centerY: progressIndicator.height / 2
|
|
||||||
radiusX: progressIndicator.width / 2 - Tokens.padding.extraSmall / 2
|
|
||||||
radiusY: progressIndicator.height / 2 - Tokens.padding.extraSmall / 2
|
|
||||||
startAngle: -90
|
|
||||||
sweepAngle: ((root.modelData.hints.value ?? 0) / 100) * 360
|
|
||||||
|
|
||||||
Behavior on sweepAngle {
|
|
||||||
Anim {
|
|
||||||
easing: Tokens.anim.emphasizedDecel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
id: appName
|
|
||||||
|
|
||||||
anchors.left: image.right
|
|
||||||
anchors.leftMargin: Tokens.spacing.small
|
|
||||||
anchors.top: parent.top
|
|
||||||
animate: true
|
|
||||||
color: Colors.palette.m3onSurfaceVariant
|
|
||||||
maximumLineCount: 1
|
|
||||||
opacity: root.expanded ? 1 : 0
|
|
||||||
text: appNameMetrics.elidedText
|
|
||||||
font: Tokens.font.label.medium
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
Anim {
|
|
||||||
type: Anim.DefaultEffects
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TextMetrics {
|
|
||||||
id: appNameMetrics
|
|
||||||
|
|
||||||
elide: Text.ElideRight
|
|
||||||
elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - Tokens.spacing.small * 3
|
|
||||||
font: appName.font
|
|
||||||
text: root.modelData.appName
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
id: summary
|
|
||||||
|
|
||||||
anchors.left: image.right
|
|
||||||
anchors.leftMargin: Tokens.spacing.small
|
|
||||||
anchors.top: parent.top
|
|
||||||
animate: true
|
|
||||||
height: implicitHeight
|
|
||||||
maximumLineCount: 1
|
|
||||||
text: summaryMetrics.elidedText
|
|
||||||
|
|
||||||
Behavior on height {
|
|
||||||
Anim {}
|
|
||||||
}
|
|
||||||
states: State {
|
|
||||||
name: "expanded"
|
|
||||||
when: root.expanded
|
|
||||||
|
|
||||||
PropertyChanges {
|
|
||||||
body.anchors.topMargin: Tokens.spacing.extraSmall
|
|
||||||
bodyPreview.anchors.topMargin: Tokens.spacing.extraSmall
|
|
||||||
summary.anchors.topMargin: Tokens.spacing.extraSmall
|
|
||||||
summary.maximumLineCount: undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
AnchorChanges {
|
|
||||||
anchors.top: appName.bottom
|
|
||||||
target: summary
|
|
||||||
}
|
|
||||||
}
|
|
||||||
transitions: Transition {
|
|
||||||
PropertyAction {
|
|
||||||
property: "maximumLineCount"
|
|
||||||
target: summary
|
|
||||||
}
|
|
||||||
|
|
||||||
Anim {
|
|
||||||
property: "topMargin"
|
|
||||||
}
|
|
||||||
|
|
||||||
AnchorAnim {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TextMetrics {
|
|
||||||
id: summaryMetrics
|
|
||||||
|
|
||||||
elide: Text.ElideRight
|
|
||||||
elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - Tokens.spacing.small * 3
|
|
||||||
font: summary.font
|
|
||||||
text: root.modelData.summary
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
id: timeSep
|
|
||||||
|
|
||||||
anchors.left: summary.right
|
|
||||||
anchors.leftMargin: Tokens.spacing.small
|
|
||||||
anchors.top: parent.top
|
|
||||||
color: Colors.palette.m3onSurfaceVariant
|
|
||||||
text: "•"
|
|
||||||
font: Tokens.font.body.small
|
|
||||||
|
|
||||||
states: State {
|
|
||||||
name: "expanded"
|
|
||||||
when: root.expanded
|
|
||||||
|
|
||||||
AnchorChanges {
|
|
||||||
anchors.left: appName.right
|
|
||||||
target: timeSep
|
|
||||||
}
|
|
||||||
}
|
|
||||||
transitions: Transition {
|
|
||||||
AnchorAnim {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
id: time
|
|
||||||
|
|
||||||
anchors.left: timeSep.right
|
|
||||||
anchors.leftMargin: Tokens.spacing.small
|
|
||||||
anchors.top: parent.top
|
|
||||||
animate: true
|
|
||||||
color: Colors.palette.m3onSurfaceVariant
|
|
||||||
font: Tokens.font.body.small
|
|
||||||
horizontalAlignment: Text.AlignLeft
|
|
||||||
text: root.modelData.timeStr
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: expandBtn
|
|
||||||
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.topMargin: -Tokens.padding.extraSmall
|
|
||||||
implicitHeight: expandIcon.implicitHeight
|
|
||||||
implicitWidth: expandIcon.implicitHeight
|
|
||||||
|
|
||||||
StateLayer {
|
|
||||||
color: root.modelData.urgency === NotificationUrgency.Critical ? Colors.palette.m3onSecondaryContainer : Colors.palette.m3onSurface
|
|
||||||
radius: Tokens.rounding.full
|
|
||||||
|
|
||||||
onClicked: root.expanded = !root.expanded
|
|
||||||
}
|
|
||||||
|
|
||||||
MaterialIcon {
|
|
||||||
id: expandIcon
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
anchors.verticalCenterOffset: root.expanded ? -1 : 1
|
|
||||||
rotation: root.expanded ? 180 : 0
|
|
||||||
text: "expand_more"
|
|
||||||
fontStyle: Tokens.font.icon.medium
|
|
||||||
|
|
||||||
Behavior on anchors.verticalCenterOffset {
|
|
||||||
Anim {}
|
|
||||||
}
|
|
||||||
Behavior on rotation {
|
|
||||||
Anim {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
id: bodyPreview
|
|
||||||
|
|
||||||
anchors.left: summary.left
|
|
||||||
anchors.right: expandBtn.left
|
|
||||||
anchors.rightMargin: Tokens.spacing.small
|
|
||||||
anchors.top: summary.bottom
|
|
||||||
animate: true
|
|
||||||
color: Colors.palette.m3onSurfaceVariant
|
|
||||||
font: Tokens.font.body.small
|
|
||||||
opacity: root.expanded ? 0 : 1
|
|
||||||
text: bodyPreviewMetrics.elidedText
|
|
||||||
textFormat: Text.MarkdownText
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
Anim {
|
|
||||||
type: Anim.DefaultEffects
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TextMetrics {
|
|
||||||
id: bodyPreviewMetrics
|
|
||||||
|
|
||||||
elide: Text.ElideRight
|
|
||||||
elideWidth: bodyPreview.width
|
|
||||||
font: bodyPreview.font
|
|
||||||
text: root.modelData.body
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
id: body
|
|
||||||
|
|
||||||
anchors.left: summary.left
|
|
||||||
anchors.right: expandBtn.left
|
|
||||||
anchors.rightMargin: Tokens.spacing.small
|
|
||||||
anchors.top: summary.bottom
|
|
||||||
animate: true
|
|
||||||
color: Colors.palette.m3onSurfaceVariant
|
|
||||||
height: text ? implicitHeight : 0
|
|
||||||
font: Tokens.font.body.medium
|
|
||||||
opacity: root.expanded ? 1 : 0
|
|
||||||
text: root.modelData.body
|
|
||||||
textFormat: Text.MarkdownText
|
|
||||||
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
Anim {
|
|
||||||
type: Anim.DefaultEffects
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onLinkActivated: link => {
|
|
||||||
if (!root.expanded)
|
|
||||||
return;
|
|
||||||
|
|
||||||
Quickshell.execDetached(["app2unit", "-O", "--", link]);
|
|
||||||
root.modelData.popup = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ButtonRow {
|
|
||||||
id: actions
|
|
||||||
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.top: body.bottom
|
|
||||||
anchors.topMargin: Tokens.spacing.small
|
|
||||||
opacity: root.expanded ? 1 : 0
|
|
||||||
spacing: Tokens.spacing.extraSmall
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
Anim {
|
|
||||||
type: Anim.DefaultEffects
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IconButton {
|
|
||||||
enabled: root.expanded
|
|
||||||
fillWidth: root.modelData.actions.length === 0
|
|
||||||
icon: "close"
|
|
||||||
font: Tokens.font.icon.builders.small.size(14).build()
|
|
||||||
inactiveColor: root.modelData.urgency === NotificationUrgency.Critical ? Colors.palette.m3secondary : Colors.layer(Colors.palette.m3surfaceContainerHighest, 2)
|
|
||||||
inactiveOnColor: root.modelData.urgency === NotificationUrgency.Critical ? Colors.palette.m3onSecondary : Colors.palette.m3onSurfaceVariant
|
|
||||||
isRound: true
|
|
||||||
padding: Tokens.padding.extraSmall
|
|
||||||
shapeMorph: true
|
|
||||||
|
|
||||||
onClicked: root.modelData.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: root.modelData.actions
|
|
||||||
|
|
||||||
TextButton {
|
|
||||||
required property var modelData
|
|
||||||
|
|
||||||
enabled: root.expanded
|
|
||||||
fillWidth: true
|
|
||||||
implicitWidth: label.implicitWidth
|
|
||||||
inactiveColor: root.modelData.urgency === NotificationUrgency.Critical ? Colors.palette.m3secondary : Colors.layer(Colors.palette.m3surfaceContainerHighest, 2)
|
|
||||||
inactiveOnColor: root.modelData.urgency === NotificationUrgency.Critical ? Colors.palette.m3onSecondary : Colors.palette.m3onSurfaceVariant
|
|
||||||
isRound: true
|
|
||||||
label.anchors.centerIn: undefined
|
|
||||||
label.anchors.left: left
|
|
||||||
label.anchors.margins: Tokens.padding.small
|
|
||||||
label.anchors.right: right
|
|
||||||
label.anchors.verticalCenter: verticalCenter
|
|
||||||
label.elide: Text.ElideRight
|
|
||||||
label.horizontalAlignment: Text.AlignHCenter
|
|
||||||
shapeMorph: true
|
|
||||||
text: modelData.text
|
|
||||||
|
|
||||||
onClicked: modelData.invoke()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IconButton {
|
|
||||||
enabled: root.expanded
|
|
||||||
fillWidth: root.modelData.actions.length === 0
|
|
||||||
icon: copyTimer.running ? "inventory" : "content_copy"
|
|
||||||
inactiveColor: root.modelData.urgency === NotificationUrgency.Critical ? Colors.palette.m3secondary : Colors.layer(Colors.palette.m3surfaceContainerHighest, 2)
|
|
||||||
inactiveOnColor: root.modelData.urgency === NotificationUrgency.Critical ? Colors.palette.m3onSecondary : Colors.palette.m3onSurfaceVariant
|
|
||||||
isRound: true
|
|
||||||
label.animate: true
|
|
||||||
padding: Tokens.padding.extraSmall
|
|
||||||
shapeMorph: true
|
|
||||||
font: Tokens.font.icon.builders.small.size(14).build()
|
|
||||||
|
|
||||||
onClicked: {
|
|
||||||
Quickshell.clipboardText = root.modelData.body;
|
|
||||||
copyTimer.restart();
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: copyTimer
|
|
||||||
|
|
||||||
interval: 3000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,7 +18,7 @@ Item {
|
|||||||
property real offsetScale: shouldBeActive ? 0 : 1
|
property real offsetScale: shouldBeActive ? 0 : 1
|
||||||
required property ShellScreen screen
|
required property ShellScreen screen
|
||||||
readonly property bool shouldBeActive: screenState.osd && Config.osd.enabled && !(screenState.utilities && Config.utilities.enabled)
|
readonly property bool shouldBeActive: screenState.osd && Config.osd.enabled && !(screenState.utilities && Config.utilities.enabled)
|
||||||
property real sidebarOffset: sidebarOrSessionVisible ? 12 : 0
|
property real sidebarOffset: sidebarOrSessionVisible ? 0 : 0
|
||||||
required property bool sidebarOrSessionVisible
|
required property bool sidebarOrSessionVisible
|
||||||
property bool sourceMuted
|
property bool sourceMuted
|
||||||
property real sourceVolume
|
property real sourceVolume
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ CustomRect {
|
|||||||
implicitWidth: ZUtils.snapCeil(450, root)
|
implicitWidth: ZUtils.snapCeil(450, root)
|
||||||
radius: Tokens.rounding.largeIncreased
|
radius: Tokens.rounding.largeIncreased
|
||||||
|
|
||||||
|
objectName: "card-rect"
|
||||||
|
|
||||||
ClipRect {
|
ClipRect {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
radius: parent.radius
|
radius: parent.radius
|
||||||
@@ -93,6 +95,8 @@ CustomRect {
|
|||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.margins: Tokens.padding.largeIncreased
|
anchors.margins: Tokens.padding.largeIncreased
|
||||||
|
|
||||||
|
objectName: "progress-layout"
|
||||||
|
|
||||||
spacing: Tokens.spacing.extraSmall
|
spacing: Tokens.spacing.extraSmall
|
||||||
orientation: Qt.Vertical
|
orientation: Qt.Vertical
|
||||||
|
|
||||||
@@ -114,7 +118,7 @@ CustomRect {
|
|||||||
|
|
||||||
CustomProgressBar {
|
CustomProgressBar {
|
||||||
fgColor: root.accent
|
fgColor: root.accent
|
||||||
implicitHeight: Tokens.padding.small
|
implicitHeight: Tokens.padding.extraSmall
|
||||||
indeterminate: isNaN(root.usage) || isNaN(root.temperature)
|
indeterminate: isNaN(root.usage) || isNaN(root.temperature)
|
||||||
value: root.temperature / 100
|
value: root.temperature / 100
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,8 +62,8 @@ CustomRect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SnapLayout {
|
SnapLayout {
|
||||||
orientation: Qt.Vertical
|
|
||||||
id: usageColumn
|
id: usageColumn
|
||||||
|
orientation: Qt.Vertical
|
||||||
|
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
anchors.verticalCenterOffset: Tokens.padding.extraSmall
|
anchors.verticalCenterOffset: Tokens.padding.extraSmall
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ CustomRect {
|
|||||||
id: sparkline
|
id: sparkline
|
||||||
|
|
||||||
property bool initialized: false
|
property bool initialized: false
|
||||||
property real smoothMax: targetMax
|
property real smoothMax: targetMax * 1.6
|
||||||
property real targetMax: 1024
|
property real targetMax: 1024
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@@ -74,7 +74,7 @@ CustomRect {
|
|||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
sparkline.targetMax = Math.max(NetworkUsage.downloadBuffer.maximum, NetworkUsage.uploadBuffer.maximum, 1024);
|
sparkline.targetMax = Math.max(NetworkUsage.downloadBuffer.maximum, NetworkUsage.uploadBuffer.maximum, 1024);
|
||||||
|
|
||||||
sparkline.smoothMax = Qt.binding(() => sparkline.targetMax);
|
sparkline.smoothMax = Qt.binding(() => sparkline.targetMax * 1.6);
|
||||||
|
|
||||||
sparkline.initialized = true;
|
sparkline.initialized = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,8 +28,7 @@ Item {
|
|||||||
|
|
||||||
Behavior on offsetScale {
|
Behavior on offsetScale {
|
||||||
Anim {
|
Anim {
|
||||||
duration: Tokens.anim.durations.expressiveDefaultSpatial
|
type: Anim.DefaultSpatial
|
||||||
easing: Tokens.anim.expressiveDefaultSpatial
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import QtQuick.Layouts
|
import QtQuick
|
||||||
import ZShell.Components
|
import ZShell.Components
|
||||||
import ZShell.Config
|
import ZShell.Config
|
||||||
import qs.Modules.Settings.Common
|
import qs.Modules.Settings.Common
|
||||||
@@ -9,9 +9,11 @@ PageBase {
|
|||||||
title: qsTr("Panels")
|
title: qsTr("Panels")
|
||||||
|
|
||||||
SnapLayout {
|
SnapLayout {
|
||||||
orientation: Qt.Vertical
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
|
|
||||||
|
orientation: Qt.Vertical
|
||||||
|
|
||||||
spacing: Tokens.spacing.extraSmall / 2
|
spacing: Tokens.spacing.extraSmall / 2
|
||||||
width: root.cappedWidth
|
width: root.cappedWidth
|
||||||
|
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|||||||
@@ -1,198 +0,0 @@
|
|||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import Quickshell
|
|
||||||
import ZShell.Config
|
|
||||||
import qs.Components
|
|
||||||
import qs.Services
|
|
||||||
import qs.Daemons
|
|
||||||
import ZShell.Components
|
|
||||||
|
|
||||||
CustomRect {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
readonly property CustomText body: (expandedContent.item as ExpandedBody)?.body ?? null
|
|
||||||
required property bool expanded
|
|
||||||
required property NotifServer.Notif modelData
|
|
||||||
readonly property real nonAnimHeight: expanded ? summary.implicitHeight + expandedContent.implicitHeight + expandedContent.anchors.topMargin + Tokens.padding.medium * 2 : summaryHeightMetrics.height
|
|
||||||
required property ScreenState screenState
|
|
||||||
|
|
||||||
color: {
|
|
||||||
const c = root.modelData?.urgency === "critical" ? Colors.palette.m3secondaryContainer : Colors.layer(Colors.palette.m3surfaceContainerHigh, 2);
|
|
||||||
return expanded ? c : Qt.alpha(c, 0);
|
|
||||||
}
|
|
||||||
implicitHeight: nonAnimHeight
|
|
||||||
radius: Tokens.rounding.large - Tokens.padding.small
|
|
||||||
state: expanded ? "expanded" : ""
|
|
||||||
|
|
||||||
Behavior on implicitHeight {
|
|
||||||
Anim {}
|
|
||||||
}
|
|
||||||
states: State {
|
|
||||||
name: "expanded"
|
|
||||||
|
|
||||||
PropertyChanges {
|
|
||||||
compactBody.anchors.margins: Tokens.padding.medium
|
|
||||||
dummySummary.anchors.margins: Tokens.padding.medium
|
|
||||||
expandedContent.anchors.margins: Tokens.padding.medium
|
|
||||||
summary.anchors.margins: Tokens.padding.medium
|
|
||||||
summary.maximumLineCount: Number.MAX_SAFE_INTEGER
|
|
||||||
summary.width: root.width - Tokens.padding.medium * 2 - timeStr.implicitWidth - Tokens.spacing.small
|
|
||||||
timeStr.anchors.margins: Tokens.padding.medium
|
|
||||||
}
|
|
||||||
}
|
|
||||||
transitions: Transition {
|
|
||||||
Anim {
|
|
||||||
properties: "margins,width,maximumLineCount"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TextMetrics {
|
|
||||||
id: summaryHeightMetrics
|
|
||||||
|
|
||||||
font: summary.font
|
|
||||||
text: " " // Use this height to prevent weird characters from changing the line height
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
id: summary
|
|
||||||
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.top: parent.top
|
|
||||||
color: root.modelData?.urgency === "critical" ? Colors.palette.m3onSecondaryContainer : Colors.palette.m3onSurface
|
|
||||||
elide: Text.ElideRight
|
|
||||||
maximumLineCount: 1
|
|
||||||
text: root.modelData?.summary ?? ""
|
|
||||||
width: parent.width
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
id: dummySummary
|
|
||||||
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.top: parent.top
|
|
||||||
text: root.modelData?.summary ?? ""
|
|
||||||
visible: false
|
|
||||||
}
|
|
||||||
|
|
||||||
WrappedLoader {
|
|
||||||
id: compactBody
|
|
||||||
|
|
||||||
anchors.left: dummySummary.right
|
|
||||||
anchors.leftMargin: Tokens.spacing.small
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.top: parent.top
|
|
||||||
shouldBeActive: !root.expanded
|
|
||||||
|
|
||||||
sourceComponent: CustomText {
|
|
||||||
color: root.modelData?.urgency === "critical" ? Colors.palette.m3secondary : Colors.palette.m3outline
|
|
||||||
elide: Text.ElideRight
|
|
||||||
text: String(root.modelData?.body ?? "").replace(/\n/g, " ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
WrappedLoader {
|
|
||||||
id: timeStr
|
|
||||||
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.top: parent.top
|
|
||||||
shouldBeActive: root.expanded
|
|
||||||
|
|
||||||
sourceComponent: CustomText {
|
|
||||||
animate: true
|
|
||||||
color: Colors.palette.m3outline
|
|
||||||
text: root.modelData?.timeStr ?? ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
WrappedLoader {
|
|
||||||
id: expandedContent
|
|
||||||
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.top: summary.bottom
|
|
||||||
anchors.topMargin: Tokens.spacing.extraSmall
|
|
||||||
shouldBeActive: root.expanded
|
|
||||||
|
|
||||||
sourceComponent: ExpandedBody {}
|
|
||||||
}
|
|
||||||
|
|
||||||
component ExpandedBody: SnapLayout {
|
|
||||||
readonly property alias body: bodyText
|
|
||||||
|
|
||||||
spacing: Tokens.spacing.medium
|
|
||||||
orientation: Qt.Vertical
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
id: bodyText
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
color: root.modelData?.urgency === "critical" ? Colors.palette.m3secondary : Colors.palette.m3onSurface
|
|
||||||
text: String(root.modelData?.body ?? "").replace(/(.)\n(?!\n)/g, "$1\n\n") || qsTr("No body given")
|
|
||||||
textFormat: Text.MarkdownText
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
|
|
||||||
onLinkActivated: link => {
|
|
||||||
Qt.openUrlExternally(link);
|
|
||||||
root.screenState.sidebar = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NotifActionList {
|
|
||||||
notif: root.modelData
|
|
||||||
}
|
|
||||||
}
|
|
||||||
component WrappedLoader: Loader {
|
|
||||||
id: comp
|
|
||||||
|
|
||||||
required property bool shouldBeActive
|
|
||||||
|
|
||||||
active: false
|
|
||||||
opacity: 0
|
|
||||||
|
|
||||||
// Makes the loader load on the same frame shouldBeActive becomes true, which ensures size is set
|
|
||||||
states: State {
|
|
||||||
name: "active"
|
|
||||||
when: comp.shouldBeActive
|
|
||||||
|
|
||||||
PropertyChanges {
|
|
||||||
comp.active: true
|
|
||||||
comp.opacity: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
transitions: [
|
|
||||||
Transition {
|
|
||||||
from: ""
|
|
||||||
to: "active"
|
|
||||||
|
|
||||||
SequentialAnimation {
|
|
||||||
PropertyAction {
|
|
||||||
property: "active"
|
|
||||||
}
|
|
||||||
|
|
||||||
Anim {
|
|
||||||
property: "opacity"
|
|
||||||
type: Anim.DefaultEffects
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Transition {
|
|
||||||
from: "active"
|
|
||||||
to: ""
|
|
||||||
|
|
||||||
SequentialAnimation {
|
|
||||||
Anim {
|
|
||||||
property: "opacity"
|
|
||||||
type: Anim.DefaultEffects
|
|
||||||
}
|
|
||||||
|
|
||||||
PropertyAction {
|
|
||||||
property: "active"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import ZShell.Config
|
|
||||||
import ZShell.Components
|
|
||||||
import qs.Components
|
|
||||||
import qs.Effects
|
|
||||||
import qs.Services
|
|
||||||
import qs.Daemons
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
required property NotifServer.Notif notif
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
implicitHeight: actionList.implicitHeight
|
|
||||||
|
|
||||||
SnapLayout {
|
|
||||||
id: actionList
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
spacing: Tokens.spacing.extraSmall
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: [
|
|
||||||
{
|
|
||||||
isClose: true
|
|
||||||
},
|
|
||||||
...(root.notif?.actions ?? []),
|
|
||||||
{
|
|
||||||
isCopy: true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
CustomRect {
|
|
||||||
id: action
|
|
||||||
|
|
||||||
required property var modelData
|
|
||||||
|
|
||||||
Layout.fillHeight: true
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.preferredWidth: implicitWidth + (actionStateLayer.pressed ? Tokens.padding.large : 0)
|
|
||||||
color: Colors.layer(Colors.palette.m3surfaceContainerHighest, 4)
|
|
||||||
implicitHeight: actionInner.implicitHeight + Tokens.padding.small
|
|
||||||
implicitWidth: actionInner.implicitWidth + Tokens.padding.medium * 2
|
|
||||||
radius: actionStateLayer.pressed ? Tokens.rounding.medium / 2 : Tokens.rounding.large
|
|
||||||
|
|
||||||
Behavior on Layout.preferredWidth {
|
|
||||||
Anim {
|
|
||||||
type: Anim.FastSpatial
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Behavior on radius {
|
|
||||||
Anim {
|
|
||||||
type: Anim.FastSpatial
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: copyTimer
|
|
||||||
|
|
||||||
interval: 3000
|
|
||||||
|
|
||||||
onTriggered: actionInner.item.text = "content_copy"
|
|
||||||
}
|
|
||||||
|
|
||||||
StateLayer {
|
|
||||||
id: actionStateLayer
|
|
||||||
|
|
||||||
onClicked: {
|
|
||||||
if (action.modelData.isClose) {
|
|
||||||
root.notif.close();
|
|
||||||
} else if (action.modelData.isCopy) {
|
|
||||||
Quickshell.clipboardText = root.notif.body;
|
|
||||||
actionInner.item.text = "inventory";
|
|
||||||
copyTimer.start();
|
|
||||||
} else if (action.modelData.invoke) {
|
|
||||||
action.modelData.invoke();
|
|
||||||
} else if (!root.notif.resident) {
|
|
||||||
root.notif.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: actionInner
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
sourceComponent: action.modelData.isClose || action.modelData.isCopy ? iconBtn : root.notif?.hasActionIcons ? iconComp : textComp
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: iconBtn
|
|
||||||
|
|
||||||
MaterialIcon {
|
|
||||||
animate: action.modelData.isCopy ?? false
|
|
||||||
color: Colors.palette.m3onSurfaceVariant
|
|
||||||
text: action.modelData.isCopy ? "content_copy" : "close"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: iconComp
|
|
||||||
|
|
||||||
IconImage {
|
|
||||||
asynchronous: true
|
|
||||||
source: Quickshell.iconPath(action.modelData.identifier)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: textComp
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
color: Colors.palette.m3onSurfaceVariant
|
|
||||||
text: action.modelData.text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,100 +7,113 @@ import QtQuick.Layouts
|
|||||||
import ZShell.Components
|
import ZShell.Components
|
||||||
import ZShell.Config
|
import ZShell.Config
|
||||||
import qs.Components
|
import qs.Components
|
||||||
import qs.Modules
|
|
||||||
import qs.Daemons
|
import qs.Daemons
|
||||||
|
import qs.Modules
|
||||||
import qs.Services
|
import qs.Services
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
readonly property int notifCount: NotifServer.list.reduce((acc, n) => n.closed ? acc : acc + 1, 0)
|
|
||||||
required property ScreenState screenState
|
required property ScreenState screenState
|
||||||
|
readonly property int notifCount: NotifServer.notClosed.length
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: Tokens.padding.small
|
anchors.margins: Tokens.padding.small
|
||||||
|
|
||||||
Component.onCompleted: NotifServer.list.forEach(n => n.popup = false)
|
Component.onCompleted: NotifServer.list.forEach(n => n.dismissPopup())
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: title
|
id: title
|
||||||
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.margins: 4
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: Tokens.padding.extraSmall
|
||||||
|
|
||||||
implicitHeight: Math.max(count.implicitHeight, titleText.implicitHeight)
|
implicitHeight: Math.max(count.implicitHeight, titleText.implicitHeight)
|
||||||
|
|
||||||
CustomText {
|
CustomText {
|
||||||
id: count
|
id: count
|
||||||
|
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.leftMargin: root.notifCount > 0 ? 0 : -width - titleText.anchors.leftMargin
|
anchors.leftMargin: root.notifCount > 0 ? 0 : -width - titleText.anchors.leftMargin
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
opacity: root.notifCount > 0 ? 1 : 0
|
||||||
|
|
||||||
|
text: root.notifCount
|
||||||
color: Colors.palette.m3outline
|
color: Colors.palette.m3outline
|
||||||
font: Tokens.font.label.large
|
font: Tokens.font.label.large
|
||||||
opacity: root.notifCount > 0 ? 1 : 0
|
|
||||||
text: root.notifCount
|
|
||||||
|
|
||||||
Behavior on anchors.leftMargin {
|
Behavior on anchors.leftMargin {
|
||||||
Anim {}
|
Anim {}
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
Anim {}
|
Anim {
|
||||||
|
type: Anim.DefaultEffects
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomText {
|
CustomText {
|
||||||
id: titleText
|
id: titleText
|
||||||
|
|
||||||
anchors.left: count.right
|
|
||||||
anchors.leftMargin: 7
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
color: Colors.palette.m3outline
|
anchors.left: count.right
|
||||||
elide: Text.ElideRight
|
anchors.right: parent.right
|
||||||
font: Tokens.font.label.large
|
anchors.leftMargin: Tokens.spacing.extraSmall
|
||||||
|
|
||||||
text: root.notifCount > 0 ? qsTr("notification%1").arg(root.notifCount === 1 ? "" : "s") : qsTr("Notifications")
|
text: root.notifCount > 0 ? qsTr("notification%1").arg(root.notifCount === 1 ? "" : "s") : qsTr("Notifications")
|
||||||
|
color: Colors.palette.m3outline
|
||||||
|
font: Tokens.font.label.large
|
||||||
|
elide: Text.ElideRight
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: clipRect
|
id: clipRect
|
||||||
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.top: title.bottom
|
anchors.top: title.bottom
|
||||||
anchors.topMargin: 10
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.topMargin: Tokens.spacing.medium
|
||||||
|
|
||||||
|
clip: true
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
active: opacity > 0
|
asynchronous: true
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
|
active: opacity > 0
|
||||||
opacity: root.notifCount > 0 ? 0 : 1
|
opacity: root.notifCount > 0 ? 0 : 1
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
Anim {}
|
|
||||||
}
|
|
||||||
sourceComponent: SnapLayout {
|
sourceComponent: SnapLayout {
|
||||||
spacing: 20
|
|
||||||
orientation: Qt.Vertical
|
orientation: Qt.Vertical
|
||||||
|
spacing: Tokens.spacing.extraLarge
|
||||||
|
|
||||||
CustomText {
|
CustomText {
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
text: qsTr("No notifications")
|
||||||
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")
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {
|
||||||
|
type: Anim.StandardExtraLarge
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomFlickable {
|
KineticFlickable {
|
||||||
id: view
|
id: view
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
contentHeight: notifList.implicitHeight
|
|
||||||
contentWidth: width
|
|
||||||
flickableDirection: Flickable.VerticalFlick
|
flickableDirection: Flickable.VerticalFlick
|
||||||
|
contentWidth: width
|
||||||
|
contentHeight: notifList.implicitHeight
|
||||||
|
|
||||||
CustomScrollBar.vertical: CustomScrollBar {
|
CustomScrollBar.vertical: CustomScrollBar {
|
||||||
flickable: view
|
flickable: view
|
||||||
@@ -109,8 +122,8 @@ Item {
|
|||||||
NotifDockList {
|
NotifDockList {
|
||||||
id: notifList
|
id: notifList
|
||||||
|
|
||||||
container: view
|
|
||||||
screenState: root.screenState
|
screenState: root.screenState
|
||||||
|
container: view
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,21 +131,19 @@ Item {
|
|||||||
Timer {
|
Timer {
|
||||||
id: clearTimer
|
id: clearTimer
|
||||||
|
|
||||||
interval: Math.max(15, Math.min(80, 69.8 - 12.3 * Math.log(NotifServer.notClosed.length)))
|
|
||||||
repeat: true
|
repeat: true
|
||||||
triggeredOnStart: true
|
triggeredOnStart: true
|
||||||
|
interval: Math.max(15, Math.min(80, 69.8 - 12.3 * Math.log(NotifServer.notClosed.length)))
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
const first = NotifServer.notClosed[0];
|
const group = NotifServer.groups[0];
|
||||||
if (!first) {
|
if (!group) {
|
||||||
stop();
|
stop();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const appName = first.appName;
|
|
||||||
let cleared = 0;
|
let cleared = 0;
|
||||||
for (const n of NotifServer.notClosed.filter(n => n.appName === appName)) {
|
for (const n of [...group.activeNotifs]) {
|
||||||
n.close();
|
n.dismiss();
|
||||||
cleared++;
|
cleared++;
|
||||||
if (cleared > 30) {
|
if (cleared > 30) {
|
||||||
interval = 5;
|
interval = 5;
|
||||||
@@ -143,33 +154,40 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
active: opacity > 0
|
asynchronous: true
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.margins: Tokens.padding.small
|
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
opacity: root.notifCount > 0 ? 1 : 0
|
anchors.bottom: parent.bottom
|
||||||
scale: root.notifCount > 0 ? 1 : 0.5
|
anchors.margins: Tokens.padding.medium
|
||||||
|
|
||||||
|
scale: root.notifCount > 0 ? 1 : 0.5
|
||||||
|
opacity: root.notifCount > 0 ? 1 : 0
|
||||||
|
active: opacity > 0
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
Anim {}
|
|
||||||
}
|
|
||||||
Behavior on scale {
|
|
||||||
Anim {}
|
|
||||||
}
|
|
||||||
sourceComponent: IconButton {
|
sourceComponent: IconButton {
|
||||||
id: clearBtn
|
id: clearBtn
|
||||||
|
|
||||||
font: Tokens.font.icon.large
|
|
||||||
icon: "clear_all"
|
icon: "clear_all"
|
||||||
|
font: Tokens.font.icon.large
|
||||||
isRound: true
|
isRound: true
|
||||||
|
|
||||||
onClicked: clearTimer.start()
|
onClicked: clearTimer.start()
|
||||||
|
|
||||||
Elevation {
|
Elevation {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
level: clearBtn.stateLayer.containsMouse ? 4 : 3
|
|
||||||
radius: parent.radius
|
radius: parent.radius
|
||||||
z: -1
|
z: -1
|
||||||
|
level: clearBtn.stateLayer.containsMouse ? 4 : 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on scale {
|
||||||
|
Anim {
|
||||||
|
type: Anim.FastSpatial
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {
|
||||||
|
type: Anim.DefaultEffects
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+119
-115
@@ -4,8 +4,10 @@ import QtQuick
|
|||||||
import Quickshell
|
import Quickshell
|
||||||
import ZShell.Components
|
import ZShell.Components
|
||||||
import ZShell.Config
|
import ZShell.Config
|
||||||
import qs.Daemons
|
import ZShell.Services
|
||||||
import qs.Components
|
import qs.Components
|
||||||
|
import qs.Components.Notifs
|
||||||
|
import qs.Daemons
|
||||||
import qs.Services
|
import qs.Services
|
||||||
|
|
||||||
LazyListView {
|
LazyListView {
|
||||||
@@ -16,121 +18,12 @@ LazyListView {
|
|||||||
|
|
||||||
anchors.left: parent?.left
|
anchors.left: parent?.left
|
||||||
anchors.right: parent?.right
|
anchors.right: parent?.right
|
||||||
asynchronous: true
|
|
||||||
cacheBuffer: 400
|
|
||||||
implicitHeight: contentHeight
|
implicitHeight: contentHeight
|
||||||
readyDelay: 1
|
|
||||||
removeDuration: Tokens.anim.durations.normal
|
|
||||||
spacing: Tokens.spacing.small
|
spacing: Tokens.spacing.small
|
||||||
useCustomViewport: true
|
readyDelay: 1
|
||||||
viewport: Qt.rect(0, container.contentY, width, container.height)
|
cacheBuffer: 400
|
||||||
|
asynchronous: true
|
||||||
delegate: Component {
|
|
||||||
MouseArea {
|
|
||||||
id: notif
|
|
||||||
|
|
||||||
readonly property bool closed: notifInner.notifCount === 0
|
|
||||||
required property int index
|
|
||||||
required property string modelData
|
|
||||||
property int startY
|
|
||||||
|
|
||||||
function closeAll(): void {
|
|
||||||
clearTimer.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyListView.preferredHeight: closed ? 0 : notifInner.nonAnimHeight
|
|
||||||
LazyListView.trackViewport: !notifInner.expanded && notifInner.nonAnimHeight < notifInner.implicitHeight
|
|
||||||
LazyListView.visibleHeight: notifInner.implicitHeight
|
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
|
||||||
cursorShape: pressed ? Qt.ClosedHandCursor : undefined
|
|
||||||
drag.axis: Drag.XAxis
|
|
||||||
drag.target: this
|
|
||||||
enabled: !closed
|
|
||||||
hoverEnabled: true
|
|
||||||
implicitHeight: notifInner.implicitHeight
|
|
||||||
opacity: LazyListView.removing || closed || LazyListView.adding ? 0 : 1
|
|
||||||
preventStealing: true
|
|
||||||
scale: LazyListView.removing || closed ? 0.6 : LazyListView.adding ? 0 : 1
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
Anim {
|
|
||||||
type: Anim.DefaultEffects
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Behavior on scale {
|
|
||||||
Anim {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Behavior on x {
|
|
||||||
Anim {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Behavior on y {
|
|
||||||
enabled: notif.LazyListView.ready
|
|
||||||
|
|
||||||
Anim {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onPositionChanged: event => {
|
|
||||||
if (pressed) {
|
|
||||||
const diffY = event.y - startY;
|
|
||||||
if (Math.abs(diffY) > Config.notifs.expandThreshold)
|
|
||||||
notifInner.toggleExpand(diffY > 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onPressed: event => {
|
|
||||||
startY = event.y;
|
|
||||||
if (event.button === Qt.RightButton)
|
|
||||||
notifInner.toggleExpand(!notifInner.expanded);
|
|
||||||
else if (event.button === Qt.MiddleButton)
|
|
||||||
closeAll();
|
|
||||||
}
|
|
||||||
onReleased: event => {
|
|
||||||
if (Math.abs(x) < width * Config.notifs.clearThreshold)
|
|
||||||
x = 0;
|
|
||||||
else
|
|
||||||
closeAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: clearTimer
|
|
||||||
|
|
||||||
interval: 15
|
|
||||||
repeat: true
|
|
||||||
triggeredOnStart: true
|
|
||||||
|
|
||||||
onTriggered: {
|
|
||||||
const notifs = NotifServer.notClosed.filter(n => n.appName === notif.modelData);
|
|
||||||
if (notifs.length === 0) {
|
|
||||||
stop();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const n of notifs.slice(0, 30))
|
|
||||||
n.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NotifGroup {
|
|
||||||
id: notifInner
|
|
||||||
|
|
||||||
container: root.container
|
|
||||||
modelData: notif.modelData
|
|
||||||
screenState: root.screenState
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
model: ScriptModel {
|
|
||||||
values: {
|
|
||||||
const map = new Map();
|
|
||||||
for (const n of NotifServer.notClosed)
|
|
||||||
map.set(n.appName, null);
|
|
||||||
for (const n of NotifServer.list)
|
|
||||||
map.set(n.appName, null);
|
|
||||||
return [...map.keys()];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onViewportAdjustNeeded: d => {
|
onViewportAdjustNeeded: d => {
|
||||||
if (contentYAnim.running)
|
if (contentYAnim.running)
|
||||||
@@ -139,10 +32,121 @@ LazyListView {
|
|||||||
contentYAnim.start();
|
contentYAnim.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useCustomViewport: true
|
||||||
|
viewport: Qt.rect(0, container.contentY, width, container.height)
|
||||||
|
|
||||||
|
removeDuration: Tokens.anim.durations.normal
|
||||||
|
|
||||||
|
model: ScriptModel {
|
||||||
|
values: [...NotifServer.groups]
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Component {
|
||||||
|
NotifSwipeArea {
|
||||||
|
id: notif
|
||||||
|
|
||||||
|
required property int index
|
||||||
|
required property NotificationGroup modelData
|
||||||
|
|
||||||
|
readonly property bool closed: card.notifs.length === 0
|
||||||
|
|
||||||
|
function toggleExpand(expand: bool): void {
|
||||||
|
if (expand) {
|
||||||
|
if (!expanded)
|
||||||
|
root.screenState.expandedNotifs.push(modelData.appName);
|
||||||
|
} else if (expanded) {
|
||||||
|
root.screenState.expandedNotifs.splice(root.screenState.expandedNotifs.indexOf(modelData.appName), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeAll(): void {
|
||||||
|
clearTimer.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onDestruction: {
|
||||||
|
if (closed && expanded)
|
||||||
|
toggleExpand(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyListView.trackViewport: !expanded && card.nonAnimHeight < card.implicitHeight
|
||||||
|
LazyListView.preferredHeight: closed ? 0 : card.nonAnimHeight
|
||||||
|
LazyListView.visibleHeight: card.implicitHeight
|
||||||
|
implicitHeight: card.implicitHeight
|
||||||
|
|
||||||
|
opacity: LazyListView.removing || closed || LazyListView.adding ? 0 : 1
|
||||||
|
scale: LazyListView.adding ? 0 : 1
|
||||||
|
|
||||||
|
expanded: root.screenState.expandedNotifs.includes(modelData.appName)
|
||||||
|
enabled: !closed
|
||||||
|
|
||||||
|
onClosedChanged: {
|
||||||
|
if (closed)
|
||||||
|
slideOut.start();
|
||||||
|
}
|
||||||
|
onRequestExpand: expand => toggleExpand(expand)
|
||||||
|
onSwiped: closeAll()
|
||||||
|
onDismissed: closeAll()
|
||||||
|
|
||||||
|
Anim {
|
||||||
|
id: slideOut
|
||||||
|
|
||||||
|
target: notif
|
||||||
|
property: "x"
|
||||||
|
to: notif.x >= 0 ? notif.width : -notif.width
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: clearTimer
|
||||||
|
|
||||||
|
interval: 15
|
||||||
|
repeat: true
|
||||||
|
triggeredOnStart: true
|
||||||
|
onTriggered: {
|
||||||
|
const notifs = [...notif.modelData.activeNotifs];
|
||||||
|
if (notifs.length === 0) {
|
||||||
|
stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const n of notifs.slice(0, 30))
|
||||||
|
n.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NotifGroupCard {
|
||||||
|
id: card
|
||||||
|
|
||||||
|
group: notif.modelData
|
||||||
|
container: root.container
|
||||||
|
expanded: notif.expanded
|
||||||
|
baseLayer: 1
|
||||||
|
onRequestExpand: expand => notif.toggleExpand(expand)
|
||||||
|
onSwiped: notif => notif.dismiss()
|
||||||
|
onLinkActivated: root.screenState.sidebar = false
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on y {
|
||||||
|
enabled: notif.LazyListView.ready
|
||||||
|
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
Anim {
|
||||||
|
type: Anim.DefaultEffects
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on scale {
|
||||||
|
Anim {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Anim {
|
Anim {
|
||||||
id: contentYAnim
|
id: contentYAnim
|
||||||
|
|
||||||
property: "contentY"
|
|
||||||
target: root.container
|
target: root.container
|
||||||
|
property: "contentY"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,251 +0,0 @@
|
|||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Services.Notifications
|
|
||||||
import ZShell.Config
|
|
||||||
import qs.Components
|
|
||||||
import qs.Services
|
|
||||||
import qs.Helpers
|
|
||||||
import qs.Daemons
|
|
||||||
import ZShell.Components
|
|
||||||
|
|
||||||
CustomRect {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
readonly property list<var> activeNotifs: notifs.filter(n => !n.closed)
|
|
||||||
readonly property string appIcon: activeNotifs.find(n => n.appIcon.length > 0)?.appIcon ?? ""
|
|
||||||
required property Flickable container
|
|
||||||
readonly property bool expanded: screenState.expandedNotifs.includes(modelData)
|
|
||||||
readonly property string image: activeNotifs.find(n => n.image.length > 0)?.image ?? ""
|
|
||||||
required property string modelData
|
|
||||||
readonly property int nonAnimHeight: {
|
|
||||||
const headerHeight = header.implicitHeight + (root.expanded ? Math.round(Tokens.spacing.extraSmall) : 0);
|
|
||||||
const columnHeight = headerHeight + notifList.layoutHeight;
|
|
||||||
return Math.round(Math.max(Config.notifs.sizes.image, columnHeight) + Tokens.padding.small * 2);
|
|
||||||
}
|
|
||||||
readonly property int notifCount: activeNotifs.length
|
|
||||||
readonly property list<var> notifs: NotifServer.list.filter(n => n.appName === modelData)
|
|
||||||
required property ScreenState screenState
|
|
||||||
readonly property int urgency: {
|
|
||||||
if (activeNotifs.find(n => n.urgency === NotificationUrgency.Critical))
|
|
||||||
return NotificationUrgency.Critical;
|
|
||||||
if (activeNotifs.find(n => n.urgency === NotificationUrgency.Normal))
|
|
||||||
return NotificationUrgency.Normal;
|
|
||||||
return NotificationUrgency.Low;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleExpand(expand: bool): void {
|
|
||||||
if (expand) {
|
|
||||||
if (!expanded)
|
|
||||||
screenState.expandedNotifs.push(modelData);
|
|
||||||
} else if (expanded) {
|
|
||||||
screenState.expandedNotifs.splice(screenState.expandedNotifs.indexOf(modelData), 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
anchors.left: parent?.left
|
|
||||||
anchors.right: parent?.right
|
|
||||||
clip: true
|
|
||||||
color: Colors.layer(Colors.palette.m3surfaceContainer, 2)
|
|
||||||
implicitHeight: nonAnimHeight
|
|
||||||
radius: Tokens.rounding.large
|
|
||||||
|
|
||||||
Behavior on implicitHeight {
|
|
||||||
Anim {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onDestruction: {
|
|
||||||
if (notifCount === 0 && expanded)
|
|
||||||
screenState.expandedNotifs.splice(screenState.expandedNotifs.indexOf(modelData), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
SnapLayout {
|
|
||||||
id: content
|
|
||||||
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.margins: Tokens.padding.small
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.top: parent.top
|
|
||||||
|
|
||||||
objectName: "notif-content"
|
|
||||||
|
|
||||||
spacing: Tokens.spacing.medium
|
|
||||||
|
|
||||||
Item {
|
|
||||||
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
|
|
||||||
implicitHeight: Config.notifs.sizes.image
|
|
||||||
implicitWidth: Config.notifs.sizes.image
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: imageComp
|
|
||||||
|
|
||||||
Image {
|
|
||||||
asynchronous: true
|
|
||||||
cache: false
|
|
||||||
fillMode: Image.PreserveAspectCrop
|
|
||||||
height: Config.notifs.sizes.image
|
|
||||||
source: Qt.resolvedUrl(root.image)
|
|
||||||
sourceSize: {
|
|
||||||
const size = Config.notifs.sizes.image * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1);
|
|
||||||
return Qt.size(size, size);
|
|
||||||
}
|
|
||||||
width: Config.notifs.sizes.image
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: appIconComp
|
|
||||||
|
|
||||||
ColoredIcon {
|
|
||||||
color: root.urgency === NotificationUrgency.Critical ? Colors.palette.m3onError : root.urgency === NotificationUrgency.Low ? Colors.palette.m3onSurface : Colors.palette.m3onSecondaryContainer
|
|
||||||
implicitSize: Math.round(Config.notifs.sizes.image * 0.6)
|
|
||||||
layer.enabled: root.appIcon.endsWith("symbolic")
|
|
||||||
source: Quickshell.iconPath(root.appIcon)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: materialIconComp
|
|
||||||
|
|
||||||
MaterialIcon {
|
|
||||||
color: root.urgency === NotificationUrgency.Critical ? Colors.palette.m3onError : root.urgency === NotificationUrgency.Low ? Colors.palette.m3onSurface : Colors.palette.m3onSecondaryContainer
|
|
||||||
text: Icons.getNotifIcon(root.activeNotifs[0]?.summary, root.urgency)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ClipRect {
|
|
||||||
anchors.fill: parent
|
|
||||||
color: root.urgency === NotificationUrgency.Critical ? Colors.palette.m3error : root.urgency === NotificationUrgency.Low ? Colors.layer(Colors.palette.m3surfaceContainerHigh, 3) : Colors.palette.m3secondaryContainer
|
|
||||||
radius: Tokens.rounding.full
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
anchors.verticalCenterOffset: sourceComponent === materialIconComp ? 1 : 0
|
|
||||||
asynchronous: true
|
|
||||||
sourceComponent: root.image ? imageComp : root.appIcon ? appIconComp : materialIconComp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
active: root.appIcon && root.image
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.right: parent.right
|
|
||||||
asynchronous: true
|
|
||||||
|
|
||||||
sourceComponent: CustomRect {
|
|
||||||
color: root.urgency === NotificationUrgency.Critical ? Colors.palette.m3error : root.urgency === NotificationUrgency.Low ? Colors.palette.m3surfaceContainerHigh : Colors.palette.m3secondaryContainer
|
|
||||||
implicitHeight: Config.notifs.sizes.badge
|
|
||||||
implicitWidth: Config.notifs.sizes.badge
|
|
||||||
radius: Tokens.rounding.full
|
|
||||||
|
|
||||||
ColoredIcon {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
color: root.urgency === NotificationUrgency.Critical ? Colors.palette.m3onError : root.urgency === NotificationUrgency.Low ? Colors.palette.m3onSurface : Colors.palette.m3onSecondaryContainer
|
|
||||||
implicitSize: Math.round(Config.notifs.sizes.badge * 0.6)
|
|
||||||
layer.enabled: root.appIcon.endsWith("symbolic")
|
|
||||||
source: Quickshell.iconPath(root.appIcon)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SnapLayout {
|
|
||||||
id: column
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
spacing: root.expanded ? Math.round(Tokens.spacing.extraSmall) : 0
|
|
||||||
orientation: Qt.Vertical
|
|
||||||
|
|
||||||
objectName: "notif-column"
|
|
||||||
|
|
||||||
Behavior on spacing {
|
|
||||||
Anim {}
|
|
||||||
}
|
|
||||||
|
|
||||||
SnapLayout {
|
|
||||||
id: header
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
spacing: Tokens.spacing.small
|
|
||||||
|
|
||||||
objectName: "notif-header"
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
color: Colors.palette.m3onSurfaceVariant
|
|
||||||
elide: Text.ElideRight
|
|
||||||
text: root.modelData
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
animate: true
|
|
||||||
color: Colors.palette.m3outline
|
|
||||||
text: root.activeNotifs[0]?.timeStr ?? ""
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomRect {
|
|
||||||
color: root.urgency === NotificationUrgency.Critical ? Colors.palette.m3error : Colors.layer(Colors.palette.m3surfaceContainerHigh, 3)
|
|
||||||
implicitHeight: groupCount.implicitHeight + Tokens.padding.extraSmall
|
|
||||||
implicitWidth: expandBtn.implicitWidth + Tokens.padding.large
|
|
||||||
radius: Tokens.rounding.full
|
|
||||||
|
|
||||||
StateLayer {
|
|
||||||
color: root.urgency === NotificationUrgency.Critical ? Colors.palette.m3onError : Colors.palette.m3onSurface
|
|
||||||
|
|
||||||
onClicked: root.toggleExpand(!root.expanded)
|
|
||||||
}
|
|
||||||
|
|
||||||
SnapLayout {
|
|
||||||
id: expandBtn
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Tokens.spacing.extraSmall
|
|
||||||
|
|
||||||
objectName: "notif-expand"
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
id: groupCount
|
|
||||||
|
|
||||||
Layout.leftMargin: Tokens.padding.extraSmall / 2
|
|
||||||
animate: true
|
|
||||||
color: root.urgency === NotificationUrgency.Critical ? Colors.palette.m3onError : Colors.palette.m3onSurfaceVariant
|
|
||||||
text: root.notifCount
|
|
||||||
}
|
|
||||||
|
|
||||||
MaterialIcon {
|
|
||||||
Layout.rightMargin: -Tokens.padding.extraSmall / 2
|
|
||||||
Layout.topMargin: root.expanded ? -Math.floor(Tokens.padding.extraSmall) : 0
|
|
||||||
color: root.urgency === NotificationUrgency.Critical ? Colors.palette.m3onError : Colors.palette.m3onSurfaceVariant
|
|
||||||
rotation: root.expanded ? 180 : 0
|
|
||||||
text: "expand_more"
|
|
||||||
|
|
||||||
Behavior on Layout.topMargin {
|
|
||||||
Anim {}
|
|
||||||
}
|
|
||||||
Behavior on rotation {
|
|
||||||
Anim {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NotifGroupList {
|
|
||||||
id: notifList
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
container: root.container
|
|
||||||
expanded: root.expanded
|
|
||||||
notifs: root.notifs
|
|
||||||
screenState: root.screenState
|
|
||||||
|
|
||||||
onRequestToggleExpand: expand => root.toggleExpand(expand)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import ZShell.Components
|
|
||||||
import ZShell.Config
|
|
||||||
import qs.Daemons
|
|
||||||
import qs.Components
|
|
||||||
import qs.Services
|
|
||||||
|
|
||||||
LazyListView {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
required property Flickable container
|
|
||||||
required property bool expanded
|
|
||||||
required property list<var> notifs
|
|
||||||
required property ScreenState screenState
|
|
||||||
|
|
||||||
signal requestToggleExpand(expand: bool)
|
|
||||||
|
|
||||||
asynchronous: true
|
|
||||||
cacheBuffer: 400
|
|
||||||
implicitHeight: contentHeight
|
|
||||||
readyDelay: 1
|
|
||||||
removeDuration: Tokens.anim.durations.normal
|
|
||||||
spacing: Math.round(Tokens.spacing.extraSmall)
|
|
||||||
useCustomViewport: true
|
|
||||||
viewport: {
|
|
||||||
tWatcher.transform; // mapToItem is not reactive so use this to trigger updates
|
|
||||||
return Qt.rect(0, container.contentY - mapToItem(container.contentItem, 0, 0).y, width, container.height);
|
|
||||||
}
|
|
||||||
|
|
||||||
delegate: Component {
|
|
||||||
MouseArea {
|
|
||||||
id: notif
|
|
||||||
|
|
||||||
required property int index
|
|
||||||
required property NotifServer.Notif modelData
|
|
||||||
property int startY
|
|
||||||
|
|
||||||
LazyListView.preferredHeight: modelData?.closed || LazyListView.removing ? 0 : notifInner.nonAnimHeight
|
|
||||||
LazyListView.visibleHeight: modelData?.closed || LazyListView.removing ? 0 : notifInner.implicitHeight
|
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
|
||||||
cursorShape: notifInner.body?.hoveredLink ? Qt.PointingHandCursor : pressed ? Qt.ClosedHandCursor : undefined
|
|
||||||
drag.axis: Drag.XAxis
|
|
||||||
drag.target: this
|
|
||||||
enabled: !(modelData?.closed ?? true)
|
|
||||||
hoverEnabled: true
|
|
||||||
implicitHeight: notifInner.implicitHeight
|
|
||||||
opacity: LazyListView.removing || LazyListView.adding ? 0 : 1
|
|
||||||
preventStealing: !root.expanded
|
|
||||||
scale: LazyListView.removing || LazyListView.adding ? 0.7 : 1
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
Anim {
|
|
||||||
type: Anim.DefaultEffects
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Behavior on scale {
|
|
||||||
Anim {}
|
|
||||||
}
|
|
||||||
Behavior on x {
|
|
||||||
Anim {}
|
|
||||||
}
|
|
||||||
Behavior on y {
|
|
||||||
enabled: notif.LazyListView.ready
|
|
||||||
|
|
||||||
Anim {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: modelData?.lock(this)
|
|
||||||
Component.onDestruction: modelData?.unlock(this)
|
|
||||||
onPositionChanged: event => {
|
|
||||||
if (pressed && !root.expanded) {
|
|
||||||
const diffY = event.y - startY;
|
|
||||||
if (Math.abs(diffY) > Config.notifs.expandThreshold)
|
|
||||||
root.requestToggleExpand(diffY > 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onPressed: event => {
|
|
||||||
startY = event.y;
|
|
||||||
if (event.button === Qt.RightButton)
|
|
||||||
root.requestToggleExpand(!root.expanded);
|
|
||||||
else if (event.button === Qt.MiddleButton)
|
|
||||||
modelData?.close();
|
|
||||||
}
|
|
||||||
onReleased: event => {
|
|
||||||
if (Math.abs(x) < width * Config.notifs.clearThreshold)
|
|
||||||
x = 0;
|
|
||||||
else
|
|
||||||
modelData?.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
ParallelAnimation {
|
|
||||||
running: notif.modelData?.closed ?? false
|
|
||||||
|
|
||||||
onFinished: notif.modelData?.unlock(notif)
|
|
||||||
|
|
||||||
Anim {
|
|
||||||
property: "opacity"
|
|
||||||
target: notif
|
|
||||||
to: 0
|
|
||||||
type: Anim.DefaultEffects
|
|
||||||
}
|
|
||||||
|
|
||||||
Anim {
|
|
||||||
property: "x"
|
|
||||||
target: notif
|
|
||||||
to: notif.x >= 0 ? notif.width : -notif.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Notif {
|
|
||||||
id: notifInner
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
expanded: root.expanded
|
|
||||||
modelData: notif.modelData
|
|
||||||
screenState: root.screenState
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
model: ScriptModel {
|
|
||||||
values: {
|
|
||||||
if (root.expanded)
|
|
||||||
return root.notifs as Array;
|
|
||||||
|
|
||||||
let count = 0;
|
|
||||||
let i = 0;
|
|
||||||
const previewNum = Config.notifs.groupPreviewNum;
|
|
||||||
while (i < root.notifs.length && count < previewNum) {
|
|
||||||
if (!(root.notifs[i]?.closed ?? true))
|
|
||||||
count++;
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return root.notifs.slice(0, i) as Array;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TransformWatcher {
|
|
||||||
id: tWatcher
|
|
||||||
|
|
||||||
a: root.container.contentItem
|
|
||||||
b: root
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,116 @@
|
|||||||
|
|
||||||
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;
|
||||||
|
|
||||||
|
// How far the deform matrix is from identity, summed over its three distinct
|
||||||
|
// entries. The matrix is applied as a panel's transform, so it has to reach
|
||||||
|
// *exactly* identity rather than merely close to it: a residue of 1e-4 scales
|
||||||
|
// a 400 logical px panel by 0.04 px, which moves a device-aligned child onto a
|
||||||
|
// fraction of a device pixel, and anything that centers itself inside the
|
||||||
|
// panel (`anchors` with `alignWhenCentered`, `Layout.HCenter`/`VCenter`) then
|
||||||
|
// antialiases. The residue also differs per entry, so different children land
|
||||||
|
// on different sub-pixel offsets - which reads as random blur.
|
||||||
|
float identityResidue(float dm00, float dm01, float dm11) {
|
||||||
|
return std::abs(dm00 - 1.0f) + std::abs(dm01) + std::abs(dm11 - 1.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
BlobRect::BlobRect(QQuickItem* parent) : BlobShape(parent) {}
|
bool nearIdentity(float dm00, float dm01, float dm11) {
|
||||||
|
return identityResidue(dm00, dm01, dm11) <= k_identityEps;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
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 {
|
||||||
|
// Pump until the matrix is *exactly* identity, not merely within epsilon:
|
||||||
|
// updatePhysics() snaps it there once it is near enough, so this stays true
|
||||||
|
// only for the frames between the last integration and that snap.
|
||||||
|
return m_physicsActive || m_retiring ||
|
||||||
|
m_stillSamples < k_settleStillSamples ||
|
||||||
|
identityResidue(m_dm00, m_dm01, m_dm11) > 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
|
const bool settled = identityResidue(m_dm00, m_dm01, m_dm11) <= 0.0f &&
|
||||||
|
!m_retiring && !m_physicsActive;
|
||||||
|
if (settled) return;
|
||||||
|
|
||||||
|
m_dm00 = 1.0f;
|
||||||
|
m_dm01 = 0.0f;
|
||||||
|
m_dm11 = 1.0f;
|
||||||
|
m_deformMatrix = QMatrix4x4();
|
||||||
|
m_dmVel00 = m_dmVel01 = m_dmVel11 = 0.0f;
|
||||||
|
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 +125,93 @@ 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) {
|
||||||
|
finishAtIdentity();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Snap the emitted matrix to exact identity while the retire tail is
|
||||||
|
// below the visible threshold, but keep retiring: ending the retire early
|
||||||
|
// hides the background while the panel animation still has a tail.
|
||||||
|
if (nearIdentity(m_dm00, m_dm01, m_dm11)) {
|
||||||
m_dm00 = 1.0f;
|
m_dm00 = 1.0f;
|
||||||
m_dm01 = 0.0f;
|
m_dm01 = 0.0f;
|
||||||
m_dm11 = 1.0f;
|
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_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 residue = identityResidue(m_dm00, m_dm01, m_dm11);
|
||||||
|
if (residue > k_identityEps) {
|
||||||
|
beginRetire();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Residue at or below the threshold is invisible, so it is written as
|
||||||
|
// exact identity - the panel transform then stops being a transform at
|
||||||
|
// all. Sampling continues below, so a new movement can restart physics.
|
||||||
|
if (residue > 0.0f) finishAtIdentity();
|
||||||
|
}
|
||||||
|
|
||||||
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 +221,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;
|
||||||
@@ -160,12 +250,27 @@ void BlobRect::updatePhysics() {
|
|||||||
m_dmVel11 = (m_dmVel11 - kStiffness * (m_dm11 - target11) * dt) * invDamp;
|
m_dmVel11 = (m_dmVel11 - kStiffness * (m_dm11 - target11) * dt) * invDamp;
|
||||||
m_dm11 += m_dmVel11 * dt;
|
m_dm11 += m_dmVel11 * dt;
|
||||||
|
|
||||||
|
// Land on exact identity as soon as the spring is within the invisible
|
||||||
|
// threshold, so the panel's contents are back on the device pixel grid -
|
||||||
|
// but keep physicsActive until trySettle() completes: the at-rest drop
|
||||||
|
// must not happen while the panel animation still has a slow tail.
|
||||||
|
if (speed < k_moveSpeed && nearIdentity(m_dm00, m_dm01, m_dm11)) {
|
||||||
|
m_dm00 = 1.0f;
|
||||||
|
m_dm01 = 0.0f;
|
||||||
|
m_dm11 = 1.0f;
|
||||||
|
m_deformMatrix = QMatrix4x4();
|
||||||
|
emit rawDeformMatrixChanged();
|
||||||
|
updateCenteredDeformMatrix();
|
||||||
|
trySettle(speed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
m_deformMatrix = QMatrix4x4(
|
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_dm01, 0, 0, m_dm01, m_dm11, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||||
emit rawDeformMatrixChanged();
|
emit rawDeformMatrixChanged();
|
||||||
updateCenteredDeformMatrix();
|
updateCenteredDeformMatrix();
|
||||||
|
|
||||||
checkAtRest(speed);
|
trySettle(speed);
|
||||||
}
|
}
|
||||||
|
|
||||||
void BlobRect::setTopLeftRadius(qreal r) {
|
void BlobRect::setTopLeftRadius(qreal r) {
|
||||||
@@ -340,16 +445,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,28 +6,96 @@
|
|||||||
|
|
||||||
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;
|
||||||
|
|
||||||
|
// How far the deform matrix is from identity, summed over its three distinct
|
||||||
|
// entries. The matrix is applied as a panel's transform, so it has to reach
|
||||||
|
// *exactly* identity rather than merely close to it: a residue of 1e-4 scales
|
||||||
|
// a 400 logical px panel by 0.04 px, which moves a device-aligned child onto a
|
||||||
|
// fraction of a device pixel, and anything that centers itself inside the
|
||||||
|
// panel (`anchors` with `alignWhenCentered`, `Layout.HCenter`/`VCenter`) then
|
||||||
|
// antialiases. The residue also differs per entry, so different children land
|
||||||
|
// on different sub-pixel offsets - which reads as random blur.
|
||||||
|
float identityResidue(float dm00, float dm01, float dm11) {
|
||||||
|
return std::abs(dm00 - 1.0f) + std::abs(dm01) + std::abs(dm11 - 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool nearIdentity(float dm00, float dm01, float dm11) {
|
||||||
|
return identityResidue(dm00, dm01, dm11) <= k_identityEps;
|
||||||
|
}
|
||||||
} // 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 {
|
||||||
|
// Pump until the matrix is *exactly* identity, not merely within epsilon:
|
||||||
|
// updatePhysics() snaps it there once it is near enough, so this stays true
|
||||||
|
// only for the frames between the last integration and that snap.
|
||||||
|
return m_physicsActive || m_retiring ||
|
||||||
|
m_stillSamples < k_settleStillSamples ||
|
||||||
|
identityResidue(m_dm00, m_dm01, m_dm11) > 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
|
const bool settled = identityResidue(m_dm00, m_dm01, m_dm11) <= 0.0f &&
|
||||||
|
!m_retiring && !m_physicsActive;
|
||||||
|
if (settled) return;
|
||||||
|
|
||||||
m_dm00 = 1.0f;
|
m_dm00 = 1.0f;
|
||||||
m_dm01 = 0.0f;
|
m_dm01 = 0.0f;
|
||||||
m_dm11 = 1.0f;
|
m_dm11 = 1.0f;
|
||||||
m_deformMatrix = QMatrix4x4();
|
m_deformMatrix = QMatrix4x4();
|
||||||
|
m_dmVel00 = m_dmVel01 = m_dmVel11 = 0.0f;
|
||||||
m_retiring = false;
|
m_retiring = false;
|
||||||
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 +104,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,
|
||||||
@@ -55,6 +139,17 @@ void BlobRect2::updatePhysics() {
|
|||||||
m_dm00 = m_retire00 + (1.0f - m_retire00) * s;
|
m_dm00 = m_retire00 + (1.0f - m_retire00) * s;
|
||||||
m_dm01 = m_retire01 - m_retire01 * s;
|
m_dm01 = m_retire01 - m_retire01 * s;
|
||||||
m_dm11 = m_retire11 + (1.0f - m_retire11) * s;
|
m_dm11 = m_retire11 + (1.0f - m_retire11) * s;
|
||||||
|
|
||||||
|
// Snap the emitted matrix to exact identity while the retire tail is
|
||||||
|
// below the visible threshold, but keep retiring: isMoving() drives the
|
||||||
|
// at-rest drop, and finishing early hides the background while the panel
|
||||||
|
// animation still has a tail.
|
||||||
|
if (nearIdentity(m_dm00, m_dm01, m_dm11)) {
|
||||||
|
m_dm00 = 1.0f;
|
||||||
|
m_dm01 = 0.0f;
|
||||||
|
m_dm11 = 1.0f;
|
||||||
|
}
|
||||||
|
|
||||||
m_deformMatrix = QMatrix4x4(
|
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_dm01, 0, 0, m_dm01, m_dm11, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||||
emit rawDeformMatrixChanged();
|
emit rawDeformMatrixChanged();
|
||||||
@@ -63,12 +158,19 @@ 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 residue = identityResidue(m_dm00, m_dm01, m_dm11);
|
||||||
// alive while active, and it is requested before anything can return early —
|
if (residue > k_identityEps) {
|
||||||
// requesting it at the end lets a shape stop being evaluated with the matrix
|
beginRetire();
|
||||||
// still off identity, which is what left the panel contents blurry.
|
return;
|
||||||
if (m_physicsActive && m_group) m_group->markDirty();
|
}
|
||||||
|
// Residue at or below the threshold is invisible, so it is written as
|
||||||
|
// exact identity - the panel transform then stops being a transform at
|
||||||
|
// all. Sampling continues below, so a new movement can restart physics.
|
||||||
|
if (residue > 0.0f) finishAtIdentity();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 +178,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 +235,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;
|
||||||
@@ -142,27 +264,30 @@ void BlobRect2::updatePhysics() {
|
|||||||
m_dmVel11 = (m_dmVel11 - kStiffness * (m_dm11 - target11) * dt) * invDamp;
|
m_dmVel11 = (m_dmVel11 - kStiffness * (m_dm11 - target11) * dt) * invDamp;
|
||||||
m_dm11 += m_dmVel11 * dt;
|
m_dm11 += m_dmVel11 * dt;
|
||||||
|
|
||||||
|
// Land on exact identity as soon as the spring is within the invisible
|
||||||
|
// threshold, so the panel's contents are back on the device pixel grid -
|
||||||
|
// but keep physicsActive until trySettle() completes: isMoving() drives the
|
||||||
|
// at-rest drop, and clearing physics early hides the background while the
|
||||||
|
// panel animation still has a slow tail.
|
||||||
|
if (speed < k_moveSpeed && nearIdentity(m_dm00, m_dm01, m_dm11)) {
|
||||||
|
m_dm00 = 1.0f;
|
||||||
|
m_dm01 = 0.0f;
|
||||||
|
m_dm11 = 1.0f;
|
||||||
|
m_deformMatrix = QMatrix4x4();
|
||||||
|
emit rawDeformMatrixChanged();
|
||||||
|
updateCenteredDeformMatrix();
|
||||||
|
trySettle(speed);
|
||||||
|
updateMoving();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
m_deformMatrix = QMatrix4x4(
|
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_dm01, 0, 0, m_dm01, m_dm11, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
||||||
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 +463,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,33 +43,32 @@ 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)
|
// Circular smooth min — the blend fillet is a true circular arc of radius k,
|
||||||
float h = max(k - abs(a - b), 0.0) / k;
|
// tangent to both surfaces (not a polynomial/squircle curve). Deviates from
|
||||||
return min(a, b) - h * h * h * k * (1.0 / 6.0);
|
// min(a, b) only in the corner region where BOTH a < k and b < k (unlike the
|
||||||
|
// cubic, which deviates over the whole band |a - b| < k and dents the surface
|
||||||
|
// inward while the shapes are still apart). Always <= min(a, b); max blend
|
||||||
|
// depth at a == b is (sqrt(2) - 1) * k.
|
||||||
|
return max(k, min(a, b)) - length(max(vec2(k) - vec2(a, b), vec2(0.0)));
|
||||||
}
|
}
|
||||||
|
|
||||||
float smax(float a, float b, float k) {
|
float smax(float a, float b, float k) {
|
||||||
float h = max(k - abs(a - b), 0.0) / k;
|
// Circular smooth max — dual of smin: -smin(-a, -b, k). Always >= max(a, b).
|
||||||
return max(a, b) + h * h * h * k * (1.0 / 6.0);
|
return min(-k, max(a, b)) + length(max(vec2(a, b) + vec2(k), vec2(0.0)));
|
||||||
}
|
}
|
||||||
|
|
||||||
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).
|
// Circular smax variant that keeps a's boundary sharp (no inward rounding at
|
||||||
// Used for the frame inner side, so the outer edge keeps its own shape and
|
// a = 0). Used for the frame outer edge so it always fills to the edges.
|
||||||
// always fills to the edges.
|
float sm =
|
||||||
float h = max(k - abs(a - b), 0.0) / k;
|
min(-k, max(a, b)) + length(max(vec2(a, b) + vec2(k), vec2(0.0)));
|
||||||
float blend = h * h * h * k * (1.0 / 6.0);
|
float blend = (sm - max(a, b)) * smoothstep(0.0, k * 0.5, -a);
|
||||||
blend *= smoothstep(0.0, k * 0.5, -a);
|
|
||||||
return max(a, b) + blend;
|
return max(a, b) + blend;
|
||||||
}
|
}
|
||||||
|
|
||||||
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 +82,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 +91,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 +110,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 +141,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,24 +155,20 @@ void main() {
|
|||||||
continue;
|
continue;
|
||||||
if ((excludeMask & (1 << j)) != 0)
|
if ((excludeMask & (1 << j)) != 0)
|
||||||
continue;
|
continue;
|
||||||
// smin only deviates from min within smoothFactor
|
// Circular smin deviates from min only where BOTH d values are < k.
|
||||||
if (abs(dArr[i] - dArr[j]) >= smoothFactor)
|
if (max(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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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,65 +178,67 @@ 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++) {
|
// Sink onset / residual overlap: how far a rect must penetrate the border
|
||||||
vec4 rect = rectData[i * 5];
|
// before the inner wall recedes to form its pocket. Too low and the wall
|
||||||
vec4 sinkProps = rectData[i * 5 + 1];
|
// recedes faster than the junction can stay convex, denting the inner edge
|
||||||
vec2 sinkSh = rectData[i * 5 + 3].xy;
|
// inward near the rect's corners; too high and the rect nestles too deep
|
||||||
|
// before the wall yields. Tuned to the circular smin blend depth: half the
|
||||||
|
// gap-closing distance, (2 - sqrt(2)) * k / 2.
|
||||||
|
float preOff = smoothFactor * (2.0 - sqrt(2.0)) * 0.5;
|
||||||
|
float s = smoothFactor * 2.0;
|
||||||
|
|
||||||
// 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
|
// The circular smax fillet has radius kFrame; when it exceeds the border
|
||||||
// outer arc is the active boundary; a smax term added there would pull the
|
// thickness it can't complete inside the border, so the sharp outer-box term
|
||||||
// arc's sides inward (curvature/tangent mismatch), so the corner would not be
|
// bleeds onto the inner edge and bulges the inner corners (worst when
|
||||||
// a circle. Only blend on the inner side, where the inner boundary is active.
|
// thickness < smoothFactor). Clamp kFrame to the thinnest side so the inner
|
||||||
float negInner = -dInner;
|
// edge stays a clean constant-radius arc. Each inner corner is bounded by
|
||||||
float dFrame = dOuter >= negInner ? dOuter
|
// its thinner adjacent side, so the global min is correct for every corner.
|
||||||
: smaxSharpA(dOuter, negInner, smoothFactor);
|
float minThick = min(min(innerTop - outerTop, outerBot - innerBot),
|
||||||
|
min(innerLeft - outerLeft, outerRight - innerRight));
|
||||||
|
float kFrame = clamp(min(smoothFactor, minThick - 1.0), 1.0, smoothFactor);
|
||||||
|
float dFrame = smaxSharpA(dOuter, -dInner, kFrame);
|
||||||
|
|
||||||
mergedSdf = smin(mergedSdf, dFrame, smoothFactor);
|
mergedSdf = smin(mergedSdf, dFrame, smoothFactor);
|
||||||
if (dFrame < minDist) {
|
if (dFrame < minDist) {
|
||||||
@@ -269,37 +246,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)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ qml_module(ZShell-components
|
|||||||
roundedmask.hpp roundedmask.cpp
|
roundedmask.hpp roundedmask.cpp
|
||||||
roundedclip.hpp roundedclip.cpp
|
roundedclip.hpp roundedclip.cpp
|
||||||
snaplayout.hpp snaplayout.cpp
|
snaplayout.hpp snaplayout.cpp
|
||||||
|
snapview.hpp snapview.cpp
|
||||||
wheelinverter.hpp
|
wheelinverter.hpp
|
||||||
LIBRARIES
|
LIBRARIES
|
||||||
Qt::Quick
|
Qt::Quick
|
||||||
|
|||||||
@@ -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")
|
||||||
@@ -16,6 +17,20 @@ qint64 originResidual(qreal logical, qreal dpr) {
|
|||||||
return std::llround((device - std::floor(device)) * 1024.0) % 1024;
|
return std::llround((device - std::floor(device)) * 1024.0) % 1024;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Snap one size hint to the device grid. `mode` is >0 to round up, <0 to round
|
||||||
|
// down, 0 to round to nearest. Infinities and NaN are left alone: they mean
|
||||||
|
// "unbounded".
|
||||||
|
qreal snapSizeHint(qreal value, qreal dpr, int mode) {
|
||||||
|
if (qIsNaN(value) || std::isinf(value)) return value;
|
||||||
|
|
||||||
|
const double exact = value * dpr;
|
||||||
|
const double snapped = mode > 0 ? std::ceil(exact - 1e-9)
|
||||||
|
: mode < 0
|
||||||
|
? std::floor(exact + 1e-9)
|
||||||
|
: static_cast<double>(std::llround(exact));
|
||||||
|
return snapped / dpr;
|
||||||
|
}
|
||||||
|
|
||||||
QPointF absoluteOrigin(const QQuickItem* item) {
|
QPointF absoluteOrigin(const QQuickItem* item) {
|
||||||
qreal x = 0.0;
|
qreal x = 0.0;
|
||||||
qreal y = 0.0;
|
qreal y = 0.0;
|
||||||
@@ -54,49 +69,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 +98,7 @@ SnapLayout::~SnapLayout() {
|
|||||||
void SnapLayout::componentComplete() {
|
void SnapLayout::componentComplete() {
|
||||||
QQuickLayout::componentComplete();
|
QQuickLayout::componentComplete();
|
||||||
|
|
||||||
|
applySpacing();
|
||||||
invalidate();
|
invalidate();
|
||||||
ensureLayoutItemsUpdated(QQuickLayout::ApplySizeHints);
|
ensureLayoutItemsUpdated(QQuickLayout::ApplySizeHints);
|
||||||
|
|
||||||
@@ -128,13 +108,182 @@ 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SnapLayout::geometryChange(
|
||||||
|
const QRectF& newGeometry, const QRectF& oldGeometry) {
|
||||||
|
Q_D(SnapLayout);
|
||||||
|
QQuickLayout::geometryChange(newGeometry, oldGeometry);
|
||||||
|
|
||||||
|
if (!isReady()) return;
|
||||||
|
|
||||||
|
const QQuickWindow* win = window();
|
||||||
|
const qreal dpr = win ? win->effectiveDevicePixelRatio() : 0.0;
|
||||||
|
if (dpr <= 0.0) return;
|
||||||
|
|
||||||
|
// Any change of our own geometry can move us: our size changed and an anchor
|
||||||
|
// on a bottom or right edge moved us with it, or an ancestor resized and the
|
||||||
|
// same anchors moved us. Children were placed relative to the origin the
|
||||||
|
// arrangement saw: their local positions carry the compensation for that
|
||||||
|
// origin's fraction of a device pixel, so after a move they land on a
|
||||||
|
// fraction - which is why a centered child is blurred by a different amount
|
||||||
|
// every time the panel opens. Re-arrange whenever the origin's residual
|
||||||
|
// changes; it is remembered on every rearrange, so this fires only when a
|
||||||
|
// re-arrange is really needed, and it converges as soon as the animation
|
||||||
|
// stops moving the layout.
|
||||||
|
const QPointF origin = absoluteOrigin(this);
|
||||||
|
if (originResidual(origin.x(), dpr) == d->originResidualX &&
|
||||||
|
originResidual(origin.y(), dpr) == d->originResidualY)
|
||||||
|
return;
|
||||||
|
|
||||||
|
rearrangeForOriginMove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Children are placed against the origin the arrangement saw, so a move that
|
||||||
|
// changes the origin's fraction of a device pixel needs a new arrangement.
|
||||||
|
// polish() defers that to the next frame, and a polish request does nothing
|
||||||
|
// while one is already pending - if no frame follows, or the pending polish is
|
||||||
|
// never delivered, the children stay placed against the old origin forever.
|
||||||
|
// Rearrange immediately instead; polish() is only the fallback for the cases
|
||||||
|
// where rearranging now is not possible.
|
||||||
|
void SnapLayout::rearrangeForOriginMove() {
|
||||||
|
Q_D(SnapLayout);
|
||||||
|
const QSizeF size(width(), height());
|
||||||
|
if (d->rearranging || size.width() <= 0.0 || size.height() <= 0.0) {
|
||||||
|
polish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rearrange(size);
|
||||||
}
|
}
|
||||||
|
|
||||||
QSizeF SnapLayout::sizeHint(Qt::SizeHint whichSizeHint) const {
|
QSizeF SnapLayout::sizeHint(Qt::SizeHint whichSizeHint) const {
|
||||||
Q_D(const SnapLayout);
|
Q_D(const SnapLayout);
|
||||||
return d->engine.sizeHint(whichSizeHint, QSizeF(), d->styleInfo);
|
const QSizeF engineHint =
|
||||||
|
d->engine.sizeHint(whichSizeHint, QSizeF(), d->styleInfo);
|
||||||
|
|
||||||
|
const QQuickWindow* win = window();
|
||||||
|
const qreal dpr = win ? win->effectiveDevicePixelRatio() : 1.0;
|
||||||
|
if (dpr <= 0.0) return engineHint;
|
||||||
|
|
||||||
|
const bool horiz = d->orientation == Qt::Horizontal;
|
||||||
|
|
||||||
|
// Qt builds implicit sizes out of whole logical pixels:
|
||||||
|
// QQuickLayout::effectiveSizeHints_helper takes `qCeil(item->implicitHeight())`
|
||||||
|
// for every child, so the sum can be an odd number of logical pixels, which is
|
||||||
|
// a half device pixel at a fractional ratio - 37 logical pixels at 1.5x is 55.5
|
||||||
|
// device pixels. A layout that sizes itself from its implicit size then has an
|
||||||
|
// edge on a fraction of a device pixel, and anchored to a bottom edge its top
|
||||||
|
// edge - and every child measured down from that top - lands off the grid.
|
||||||
|
//
|
||||||
|
// The preferred size is therefore summed here in device pixels: each child's
|
||||||
|
// own hint rounded up to the device grid, plus its Layout margins and the
|
||||||
|
// device-normalized spacing. That is exactly what the arrangement writes, so
|
||||||
|
// the layout is neither larger than its children (dead space, and a Flickable
|
||||||
|
// that reports more content height than the content occupies) nor smaller
|
||||||
|
// (the last child cut off).
|
||||||
|
qint64 mainExtentDev = 0;
|
||||||
|
qint64 crossDev = 0;
|
||||||
|
qint64 maxMainDev = 0;
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < d->engine.itemCount(); ++i) {
|
||||||
|
const SnapGridLayoutItem* gridItem =
|
||||||
|
static_cast<const SnapGridLayoutItem*>(d->engine.itemAt(i));
|
||||||
|
QQuickItem* child = gridItem->layoutItem();
|
||||||
|
if (!child || shouldIgnoreItem(child)) continue;
|
||||||
|
|
||||||
|
QQuickLayoutAttached* info = attachedLayoutObject(child, false);
|
||||||
|
const QMarginsF margins = info ? info->effectiveQMargins()
|
||||||
|
: QMarginsF(0, 0, 0, 0);
|
||||||
|
|
||||||
|
// The engine's per-child hints are authoritative: they follow
|
||||||
|
// Layout.preferredWidth/Height when those are set, otherwise the child's
|
||||||
|
// implicit size, otherwise the first width and height it had, and they
|
||||||
|
// include the child's Layout margins. They are also cached, so a nested
|
||||||
|
// layout contributes its own preferred size even before it is polished.
|
||||||
|
const QSizeF* hints = gridItem->effectiveSizeHints();
|
||||||
|
|
||||||
|
const qreal mainMargin = horiz ? margins.left() + margins.right()
|
||||||
|
: margins.top() + margins.bottom();
|
||||||
|
const qreal crossMargin = horiz ? margins.top() + margins.bottom()
|
||||||
|
: margins.left() + margins.right();
|
||||||
|
|
||||||
|
qreal mainHint = (horiz ? hints[Qt::PreferredSize].width()
|
||||||
|
: hints[Qt::PreferredSize].height()) -
|
||||||
|
mainMargin;
|
||||||
|
qreal crossHint = (horiz ? hints[Qt::PreferredSize].height()
|
||||||
|
: hints[Qt::PreferredSize].width()) -
|
||||||
|
crossMargin;
|
||||||
|
|
||||||
|
// The engine hint is `qCeil` of the implicit size, so it can sit up to a
|
||||||
|
// whole logical pixel above it - a device pixel per child at 1.5x, which
|
||||||
|
// adds up to dead space in a long column. When the child's own implicit
|
||||||
|
// size is known and the hint is just that ceiling, measure the implicit
|
||||||
|
// size and let the device rounding be the only rounding.
|
||||||
|
const qreal ownMain = horiz ? child->implicitWidth()
|
||||||
|
: child->implicitHeight();
|
||||||
|
if (ownMain > 0.0 && mainHint <= ownMain + 1.0) mainHint = ownMain;
|
||||||
|
|
||||||
|
const qreal ownCross = horiz ? child->implicitHeight()
|
||||||
|
: child->implicitWidth();
|
||||||
|
if (ownCross > 0.0 && crossHint <= ownCross + 1.0) crossHint = ownCross;
|
||||||
|
|
||||||
|
if (!std::isfinite(mainHint) || mainHint < 0.0) mainHint = 0.0;
|
||||||
|
if (!std::isfinite(crossHint) || crossHint < 0.0) crossHint = 0.0;
|
||||||
|
|
||||||
|
// Rounding up keeps the hint from ever under-sizing its contents.
|
||||||
|
const qint64 mainDev = qint64(std::ceil(mainHint * dpr - 1e-9));
|
||||||
|
const qint64 mainMarginDev = qint64(std::ceil(mainMargin * dpr - 1e-9));
|
||||||
|
const qint64 crossDevForChild =
|
||||||
|
qint64(std::ceil(crossHint * dpr - 1e-9)) +
|
||||||
|
qint64(std::ceil(crossMargin * dpr - 1e-9));
|
||||||
|
|
||||||
|
mainExtentDev += mainDev + mainMarginDev;
|
||||||
|
maxMainDev = (std::max)(maxMainDev, mainDev + mainMarginDev);
|
||||||
|
crossDev = (std::max)(crossDev, crossDevForChild);
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
|
||||||
|
qreal space = d->spacingRequest;
|
||||||
|
if (qIsNaN(space)) space = d->styleInfo->spacing(d->orientation);
|
||||||
|
const qint64 gapDev = dpr > 0.0 ? qint64(std::llround(space * dpr))
|
||||||
|
: qint64(space);
|
||||||
|
|
||||||
|
if (uniformCellSizes() && count > 0)
|
||||||
|
mainExtentDev = maxMainDev * qint64(count);
|
||||||
|
if (count > 1) mainExtentDev += gapDev * qint64(count - 1);
|
||||||
|
|
||||||
|
const QSizeF preferred(
|
||||||
|
horiz ? qreal(mainExtentDev) / dpr : qreal(crossDev) / dpr,
|
||||||
|
horiz ? qreal(crossDev) / dpr : qreal(mainExtentDev) / dpr);
|
||||||
|
|
||||||
|
if (whichSizeHint == Qt::PreferredSize) return preferred;
|
||||||
|
|
||||||
|
if (whichSizeHint == Qt::MinimumSize) {
|
||||||
|
// Minimums round up, but never past the preferred size, or cells would
|
||||||
|
// grow past what the arrangement writes.
|
||||||
|
const QSizeF minimum(
|
||||||
|
snapSizeHint(engineHint.width(), dpr, 1),
|
||||||
|
snapSizeHint(engineHint.height(), dpr, 1));
|
||||||
|
return QSizeF(
|
||||||
|
(std::min)(minimum.width(), preferred.width()),
|
||||||
|
(std::min)(minimum.height(), preferred.height()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Maximums round down, but never below the preferred size: a layout is always
|
||||||
|
// allowed to be as large as its contents need.
|
||||||
|
const QSizeF maximum(
|
||||||
|
snapSizeHint(engineHint.width(), dpr, -1),
|
||||||
|
snapSizeHint(engineHint.height(), dpr, -1));
|
||||||
|
return QSizeF(
|
||||||
|
(std::max)(maximum.width(), preferred.width()),
|
||||||
|
(std::max)(maximum.height(), preferred.height()));
|
||||||
}
|
}
|
||||||
|
|
||||||
Qt::Orientation SnapLayout::orientation() const {
|
Qt::Orientation SnapLayout::orientation() const {
|
||||||
@@ -169,16 +318,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 +408,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 +460,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 +489,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;
|
||||||
@@ -329,24 +504,33 @@ void SnapLayout::itemVisibilityChanged(QQuickItem* item) {
|
|||||||
|
|
||||||
void SnapLayout::itemGeometryChanged(
|
void SnapLayout::itemGeometryChanged(
|
||||||
QQuickItem* item, QQuickGeometryChange change, const QRectF& oldGeometry) {
|
QQuickItem* item, QQuickGeometryChange change, const QRectF& oldGeometry) {
|
||||||
Q_UNUSED(item);
|
Q_UNUSED(item)
|
||||||
Q_UNUSED(oldGeometry);
|
Q_UNUSED(change)
|
||||||
if (!isReady() || !change.positionChange()) return;
|
Q_UNUSED(oldGeometry)
|
||||||
|
if (!isReady()) return;
|
||||||
|
|
||||||
Q_D(SnapLayout);
|
|
||||||
const QQuickWindow* win = window();
|
const QQuickWindow* win = window();
|
||||||
const qreal dpr = win ? win->effectiveDevicePixelRatio() : 0.0;
|
const qreal dpr = win ? win->effectiveDevicePixelRatio() : 0.0;
|
||||||
if (dpr <= 0.0) return;
|
if (dpr <= 0.0) return;
|
||||||
|
|
||||||
|
// An ancestor changing size can move us too - a layout anchored to a bottom
|
||||||
|
// edge slides when its parent grows - so only the origin's fraction of a
|
||||||
|
// device pixel matters, not which kind of change reached us.
|
||||||
|
Q_D(SnapLayout);
|
||||||
const QPointF origin = absoluteOrigin(this);
|
const QPointF origin = absoluteOrigin(this);
|
||||||
if (originResidual(origin.x(), dpr) == d->originResidualX &&
|
if (originResidual(origin.x(), dpr) == d->originResidualX &&
|
||||||
originResidual(origin.y(), dpr) == d->originResidualY)
|
originResidual(origin.y(), dpr) == d->originResidualY)
|
||||||
return;
|
return;
|
||||||
polish();
|
|
||||||
|
rearrangeForOriginMove();
|
||||||
}
|
}
|
||||||
|
|
||||||
void SnapLayout::updateAncestorListeners() {
|
void SnapLayout::updateAncestorListeners() {
|
||||||
Q_D(SnapLayout);
|
Q_D(SnapLayout);
|
||||||
|
|
||||||
|
const QFlags<QQuickItemPrivate::ChangeType> mask =
|
||||||
|
QQuickItemPrivate::Geometry;
|
||||||
|
|
||||||
QVector<QQuickItem*> current;
|
QVector<QQuickItem*> current;
|
||||||
for (QQuickItem* it = parentItem(); it; it = it->parentItem()) {
|
for (QQuickItem* it = parentItem(); it; it = it->parentItem()) {
|
||||||
if (qobject_cast<SnapLayout*>(it)) break;
|
if (qobject_cast<SnapLayout*>(it)) break;
|
||||||
@@ -355,15 +539,34 @@ void SnapLayout::updateAncestorListeners() {
|
|||||||
|
|
||||||
for (const QPointer<QQuickItem>& ancestor :
|
for (const QPointer<QQuickItem>& ancestor :
|
||||||
std::as_const(d->ancestorListeners)) {
|
std::as_const(d->ancestorListeners)) {
|
||||||
if (ancestor && !current.contains(ancestor.data()))
|
if (ancestor && !current.contains(ancestor.data())) {
|
||||||
QQuickItemPrivate::get(ancestor.data())
|
QQuickItemPrivate::get(ancestor.data())
|
||||||
->removeItemChangeListener(this, QQuickItemPrivate::Geometry);
|
->removeItemChangeListener(
|
||||||
|
this, d->ancestorMasks.value(ancestor.data()));
|
||||||
|
d->ancestorMasks.remove(ancestor.data());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Qt appends one registration per addItemChangeListener call, so only the
|
||||||
|
// bits that are not registered yet may be added; re-adding the same mask on
|
||||||
|
// every rearrange would duplicate listeners without bound.
|
||||||
for (QQuickItem* ancestor : std::as_const(current)) {
|
for (QQuickItem* ancestor : std::as_const(current)) {
|
||||||
if (!d->ancestorListeners.contains(ancestor))
|
const auto have = d->ancestorMasks.value(ancestor);
|
||||||
|
const auto missing = mask & ~have;
|
||||||
|
const auto extra = have & ~mask;
|
||||||
|
|
||||||
|
if (extra != 0) {
|
||||||
|
QQuickItemPrivate::get(ancestor)->removeItemChangeListener(
|
||||||
|
this, extra);
|
||||||
|
}
|
||||||
|
if (missing != 0) {
|
||||||
QQuickItemPrivate::get(ancestor)->addItemChangeListener(
|
QQuickItemPrivate::get(ancestor)->addItemChangeListener(
|
||||||
this, QQuickItemPrivate::Geometry);
|
this, missing);
|
||||||
|
}
|
||||||
|
|
||||||
|
d->ancestorMasks[ancestor] = mask;
|
||||||
}
|
}
|
||||||
|
|
||||||
d->ancestorListeners.clear();
|
d->ancestorListeners.clear();
|
||||||
d->ancestorListeners.reserve(current.size());
|
d->ancestorListeners.reserve(current.size());
|
||||||
for (QQuickItem* ancestor : std::as_const(current))
|
for (QQuickItem* ancestor : std::as_const(current))
|
||||||
@@ -374,13 +577,193 @@ void SnapLayout::clearAncestorListeners() {
|
|||||||
Q_D(SnapLayout);
|
Q_D(SnapLayout);
|
||||||
for (const QPointer<QQuickItem>& ancestor :
|
for (const QPointer<QQuickItem>& ancestor :
|
||||||
std::as_const(d->ancestorListeners)) {
|
std::as_const(d->ancestorListeners)) {
|
||||||
if (ancestor)
|
if (ancestor) {
|
||||||
QQuickItemPrivate::get(ancestor.data())
|
QQuickItemPrivate::get(ancestor.data())
|
||||||
->removeItemChangeListener(this, QQuickItemPrivate::Geometry);
|
->removeItemChangeListener(
|
||||||
|
this, d->ancestorMasks.value(ancestor.data()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
d->ancestorMasks.clear();
|
||||||
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;
|
||||||
|
|
||||||
|
// The layout's own main extent, rounded down: no child is placed past it, so
|
||||||
|
// a Flickable that takes this layout's height as its content height can still
|
||||||
|
// reach the last child.
|
||||||
|
const QSizeF self = size();
|
||||||
|
const qint64 layoutMainExtentDev =
|
||||||
|
qint64(std::floor((horiz ? self.width() : self.height()) * dpr + 1e-6));
|
||||||
|
|
||||||
|
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 mainFarExact = std::llround(mainExact + mainSizeExact);
|
||||||
|
|
||||||
|
// An aligned child whose cell is its own implicit size is sized from that
|
||||||
|
// implicit size. The engine recomputes cells from animated geometry, so a
|
||||||
|
// cell that is mathematically 41 can arrive as 40.9999999999; when the
|
||||||
|
// device pixel ratio puts 41 exactly on half a device pixel, that epsilon
|
||||||
|
// flips the rounding from pass to pass and the child visibly resizes while
|
||||||
|
// it only moves. Children that fill their axis, or whose cell has grown
|
||||||
|
// past their implicit size, keep the engine-derived size so that animated
|
||||||
|
// fills still step through device pixels.
|
||||||
|
QQuickLayoutAttached* info = attachedLayoutObject(child, false);
|
||||||
|
const qreal mainOwn = horiz ? child->implicitWidth()
|
||||||
|
: child->implicitHeight();
|
||||||
|
const qreal crossOwn = horiz ? child->implicitHeight()
|
||||||
|
: child->implicitWidth();
|
||||||
|
const bool fillsMain = info &&
|
||||||
|
(horiz ? info->fillWidth() : info->fillHeight());
|
||||||
|
const bool fillsCross =
|
||||||
|
info && (horiz ? info->fillHeight() : info->fillWidth());
|
||||||
|
|
||||||
|
const auto snappedOwn = [&](bool fills, qreal cellSize, qreal ownSize) {
|
||||||
|
if (fills || ownSize <= 0.0 || qAbs(cellSize - ownSize) >= 1.0)
|
||||||
|
return qint64(-1);
|
||||||
|
// Round up, like Qt's `qCeil` of implicit sizes: a child is never
|
||||||
|
// smaller than the content it holds, and this is the same rounding
|
||||||
|
// sizeHint() sums to compute the layout's own preferred size, so the
|
||||||
|
// children fill the layout without slack and without overflow.
|
||||||
|
return (std::max)(qint64(std::ceil(ownSize * dpr - 1e-9)),
|
||||||
|
qint64(0));
|
||||||
|
};
|
||||||
|
|
||||||
|
const qint64 mainOwnDev =
|
||||||
|
snappedOwn(fillsMain, horiz ? cell.width() : cell.height(), mainOwn);
|
||||||
|
const qint64 crossOwnDev = snappedOwn(
|
||||||
|
fillsCross, horiz ? cell.height() : cell.width(), crossOwn);
|
||||||
|
|
||||||
|
// The size along the main axis. An implicit-sized child keeps its own
|
||||||
|
// implicit size, which carries the device-pixel parity ZUtils chose for
|
||||||
|
// it. Rounding is not neutral: an odd logical size at 1.5x is exactly half
|
||||||
|
// a device pixel and always rounds up, so left unchecked every child adds
|
||||||
|
// half a pixel and a long list ends outside the layout - a Flickable reads
|
||||||
|
// the layout's height as the end of the content and the last rows become
|
||||||
|
// unreachable. Corrections are made in steps of two device pixels: an item
|
||||||
|
// that centers its own content (Layout.HCenter / Layout.VCenter,
|
||||||
|
// anchors.centerIn) lands on whole device pixels only while its device
|
||||||
|
// size keeps that parity, so a one-pixel correction moves its centered
|
||||||
|
// children onto half a pixel and they antialias. Drift from the engine's
|
||||||
|
// edge is bounded at two device pixels, and no child is placed past the
|
||||||
|
// layout's own edge - a layout with less room than its children needs
|
||||||
|
// overflows in Qt too, so the engine's own edge is never pulled back.
|
||||||
|
const qint64 mainFarCeil =
|
||||||
|
(std::max)(mainFarExact, layoutMainExtentDev);
|
||||||
|
const qint64 mainLimit =
|
||||||
|
(std::min)(mainFarExact + 2, mainFarCeil) - mainNear;
|
||||||
|
|
||||||
|
qint64 width = engineWidth;
|
||||||
|
qint64 height = engineHeight;
|
||||||
|
const qint64 mainSizeWritten =
|
||||||
|
mainOwnDev >= 0 ? mainOwnDev
|
||||||
|
: (std::max)(mainFarExact - mainNear, qint64(0));
|
||||||
|
if (horiz)
|
||||||
|
width = mainSizeWritten;
|
||||||
|
else
|
||||||
|
height = mainSizeWritten;
|
||||||
|
|
||||||
|
if (crossOwnDev >= 0) {
|
||||||
|
if (horiz)
|
||||||
|
height = crossOwnDev;
|
||||||
|
else
|
||||||
|
width = crossOwnDev;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep the size already written unless the engine asks for a different
|
||||||
|
// one, or something else resized the child. An aligned size stays
|
||||||
|
// aligned wherever the layout moves, and resizing again costs the child
|
||||||
|
// a relayout: an Image reloads its texture, a nested layout rearranges.
|
||||||
|
const auto stored = d->childSizes.constFind(child);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A remembered size can predate the layout being sized, so the limit is
|
||||||
|
// applied to whatever ends up being written, keeping the parity of the
|
||||||
|
// size this child is asking for.
|
||||||
|
qint64& mainAxisSize = horiz ? width : height;
|
||||||
|
const int mainParity = int(mainAxisSize % 2);
|
||||||
|
if (mainAxisSize > mainLimit) {
|
||||||
|
mainAxisSize = mainLimit;
|
||||||
|
if (int(mainAxisSize % 2) != mainParity) --mainAxisSize;
|
||||||
|
}
|
||||||
|
mainAxisSize = (std::max)(mainAxisSize, qint64(0));
|
||||||
|
|
||||||
|
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 +791,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))
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user