Author SHA1 Message Date
zach 11e7aac195 feat: better notification storage and image caching + blobs
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 11s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 12s
JS/TS / lint (pull_request) Successful in 10s
Python / static (pull_request) Failing after 27s
Rust / build (pull_request) Successful in 54s
Python / verify (pull_request) Failing after 1m26s
Rust / fmt (pull_request) Successful in 46s
Rust / clippy (pull_request) Successful in 1m6s
2026-11-02 19:00:43 +01:00
zach 789305286a Merge branch 'main' into feat/device-pixel-alignment
C++ / fmt (pull_request) Failing after 6s
C++ / build (pull_request) Failing after 3m19s
C++ / clang-tidy (pull_request) Failing after 3m17s
JS/TS / fmt (pull_request) Successful in 10s
JS/TS / lint (pull_request) Successful in 32s
Python / static (pull_request) Successful in 28s
Rust / build (pull_request) Successful in 49s
Python / verify (pull_request) Failing after 1m20s
Rust / fmt (pull_request) Successful in 33s
Rust / clippy (pull_request) Successful in 57s
2026-11-01 12:50:58 +01:00
zach f8101b2807 feat: sql database for notifications 2026-11-01 12:43:21 +01:00
zach 017d7073d2 Merge branch 'main' into feat/device-pixel-alignment
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 3m41s
C++ / clang-tidy (pull_request) Failing after 3m39s
JS/TS / fmt (pull_request) Successful in 13s
JS/TS / lint (pull_request) Successful in 11s
Python / static (pull_request) Successful in 34s
Rust / build (pull_request) Successful in 49s
Python / verify (pull_request) Failing after 1m29s
Rust / fmt (pull_request) Successful in 42s
Rust / clippy (pull_request) Successful in 1m2s
2026-11-01 01:57:30 +01:00
zach 8271e5d013 feat: testing new kinetic scroller for notifications in sidebar 2026-11-01 01:57:13 +01:00
zach 566b71d386 feat: drawing popout now clips strokes again + add redo + add undo and redo keybinds
C++ / fmt (pull_request) Failing after 6s
C++ / build (pull_request) Failing after 12s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 9s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Successful in 28s
Python / verify (pull_request) Failing after 1m41s
Rust / build (pull_request) Successful in 1m15s
Rust / fmt (pull_request) Successful in 42s
Rust / clippy (pull_request) Successful in 1m6s
2026-10-30 14:16:19 +01:00
zach c5608dc9b9 fix: blobs now allow to be drawn in device pixels to stay sharp
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 9s
JS/TS / lint (pull_request) Successful in 25s
Python / static (pull_request) Successful in 34s
Rust / build (pull_request) Successful in 51s
Python / verify (pull_request) Failing after 1m25s
Rust / fmt (pull_request) Successful in 39s
Rust / clippy (pull_request) Successful in 59s
2026-10-30 01:52:33 +01:00
zach 78a9577209 Merge branch 'main' into feat/device-pixel-alignment
C++ / fmt (pull_request) Failing after 6s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 7s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Successful in 26s
Rust / build (pull_request) Successful in 54s
Python / verify (pull_request) Failing after 1m29s
Rust / fmt (pull_request) Successful in 36s
Rust / clippy (pull_request) Successful in 50s
2026-10-29 20:11:33 +01:00
zach 90184bff58 Merge pull request 'feat: video wallpapers' (#163) from feat/video-wallpaper into main
Reviewed-on: #163
2026-10-29 19:55:05 +01:00
zach abdde2450d fix: don't use lambda expression
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 11s
JS/TS / fmt (pull_request) Successful in 7s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Successful in 30s
Rust / build (pull_request) Successful in 50s
Python / verify (pull_request) Failing after 1m27s
Rust / fmt (pull_request) Successful in 31s
Rust / clippy (pull_request) Successful in 58s
2026-10-29 19:50:02 +01:00
zach fd05b3d4c4 Merge branch 'main' into feat/video-wallpaper
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 9s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 13s
JS/TS / lint (pull_request) Successful in 8s
Python / static (pull_request) Failing after 37s
Rust / build (pull_request) Successful in 50s
Python / verify (pull_request) Failing after 1m28s
Rust / fmt (pull_request) Successful in 39s
Rust / clippy (pull_request) Successful in 1m5s
2026-10-29 19:03:10 +01:00
zach 067291f478 fix: lockscreen notifdock
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 9s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 7s
JS/TS / lint (pull_request) Successful in 14s
Python / static (pull_request) Failing after 27s
Rust / build (pull_request) Successful in 47s
Python / verify (pull_request) Failing after 1m23s
Rust / fmt (pull_request) Successful in 35s
Rust / clippy (pull_request) Successful in 53s
2026-10-29 18:43:50 +01:00
zach 395d1dad3d fix: remove duped property 2026-10-29 17:51:20 +01:00
zach fe0ce991da feat: initial commit for pixel-snapping components and helpers 2026-10-29 17:47:19 +01:00
zach 858e4e5b12 rebase to feat/video-wallpapers 2026-10-29 17:46:27 +01:00
zach ed7e977eb5 fix: remove more uses of cliprect and finally fix workspace's layer texture 2026-10-29 17:44:19 +01:00
zach 4252812e82 fix: don't link eglDestroyImageKHR 2026-10-29 17:44:19 +01:00
zach a893e6cbe6 feat: add vaapi path for video wallpapers on generic gpus 2026-10-29 17:44:19 +01:00
zach 3792e9d73f feat: add wallpaper transitions, store crop in database per-wallpaper, move CUDA context creation to async thread 2026-10-29 17:44:19 +01:00
zach 906b15b092 fix: don't store dbPath as const 2026-10-29 17:44:19 +01:00
zach 811ad6b7f4 fix: restore code removed by incorrect replacement command 2026-10-29 17:44:19 +01:00
zach 76f5a23c2a feat: skip two-way copy for cuda devices when decoding video wallpapers + cache wallpapers as blobs in new database 2026-10-29 17:44:19 +01:00
zach c0744b29b6 chore: suppress more warnings 2026-10-29 17:44:19 +01:00
zach 5896256372 feat: initial video wallpaper implementation 2026-10-29 17:44:19 +01:00
zach 9cbb90f9e1 feat: initial commit for pixel-snapping components and helpers 2026-10-29 17:34:57 +01:00
zach 9d8bde1413 fix: update for hyprland 0.57 2026-10-29 11:59:50 +01:00
230 changed files with 10859 additions and 3993 deletions
+3
View File
@@ -0,0 +1,3 @@
indent: tabs
list_expansion: favour-expansion
definitions: [Plugins/cmake/qml-module.cmake, Plugins/gersemi/qt-add-shaders.cmake]
+1 -1
View File
@@ -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;
} }
+44 -10
View File
@@ -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
+5 -5
View File
@@ -96,9 +96,12 @@ 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 {
id: dot
property real implicitSize property real implicitSize
anchors.right: parent.right anchors.right: parent.right
@@ -124,8 +127,7 @@ ProgressBar {
anchors.left: parent.left anchors.left: parent.left
Behavior on implicitWidth { Behavior on implicitWidth {
Anim { Anim {}
}
} }
Component.onCompleted: implicitWidth = Qt.binding(() => parent.width * root.visualPosition) Component.onCompleted: implicitWidth = Qt.binding(() => parent.width * root.visualPosition)
@@ -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
@@ -222,8 +223,7 @@ ProgressBar {
} }
} }
Behavior on color { Behavior on color {
CAnim { CAnim {}
}
} }
Anim on waveProgress { Anim on waveProgress {
duration: root.waveDuration duration: root.waveDuration
+6 -6
View File
@@ -2,10 +2,10 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Templates import QtQuick.Templates
import ZShell.Components
import ZShell import ZShell
import qs.Components import ZShell.Components
import ZShell.Config import ZShell.Config
import qs.Components
import qs.Services import qs.Services
Slider { Slider {
@@ -28,7 +28,7 @@ Slider {
signal interaction(v: real) signal interaction(v: real)
implicitHeight: 12 implicitHeight: ZUtils.snap(12, root)
implicitWidth: 200 implicitWidth: 200
contentItem: Item { contentItem: Item {
@@ -123,7 +123,7 @@ Slider {
bottomRightRadius: Tokens.rounding.extraSmall / 2 bottomRightRadius: Tokens.rounding.extraSmall / 2
color: root.fgColor color: root.fgColor
implicitHeight: root.height implicitHeight: root.height
implicitWidth: root.filledWidth implicitWidth: ZUtils.snap(root.filledWidth, root)
radius: root.radius radius: root.radius
topRightRadius: Tokens.rounding.extraSmall / 2 topRightRadius: Tokens.rounding.extraSmall / 2
} }
@@ -137,8 +137,8 @@ Slider {
frequency: root.waveFrequency frequency: root.waveFrequency
fullLength: root.width - handle.implicitWidth - handle.anchors.leftMargin fullLength: root.width - handle.implicitWidth - handle.anchors.leftMargin
implicitHeight: lineWidth * amplitudeMultiplier * 2 + lineWidth implicitHeight: lineWidth * amplitudeMultiplier * 2 + lineWidth
implicitWidth: root.filledWidth implicitWidth: ZUtils.snap(root.filledWidth, root)
lineWidth: root.height * 0.7 lineWidth: ZUtils.snap(root.height * 0.7, root)
startX: x startX: x
Behavior on color { Behavior on color {
+3 -2
View File
@@ -1,4 +1,5 @@
import QtQuick import QtQuick
import ZShell
import ZShell.Config import ZShell.Config
import qs.Services import qs.Services
@@ -15,9 +16,9 @@ ButtonBase {
const h = label.implicitHeight + padding * 2; const h = label.implicitHeight + padding * 2;
if (h % 2 !== 0) if (h % 2 !== 0)
return h + 1; return h + 1;
return h; return ZUtils.snap(h, root);
} }
implicitWidth: implicitHeight implicitWidth: ZUtils.snap(implicitHeight, root)
inactiveColor: { inactiveColor: {
if (!isToggle && type === IconButton.Filled) if (!isToggle && type === IconButton.Filled)
return Colors.palette.m3primary; return Colors.palette.m3primary;
+3 -1
View File
@@ -1,5 +1,6 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Components
import ZShell.Config import ZShell.Config
import qs.Services import qs.Services
@@ -34,10 +35,11 @@ ButtonBase {
} }
verticalPadding: Tokens.padding.extraSmall verticalPadding: Tokens.padding.extraSmall
RowLayout { SnapLayout {
id: row id: row
anchors.centerIn: parent anchors.centerIn: parent
spacing: Tokens.spacing.small spacing: Tokens.spacing.small
MaterialIcon { MaterialIcon {
+23
View File
@@ -0,0 +1,23 @@
import QtQuick
import qs.Services
Flickable {
id: root
interactive: !ShellState.forActive().isDrawing
rebound: Transition {
Anim {
properties: "x,y"
type: Anim.FastEffects
}
}
readonly property KineticWheelScroller _scroller: KineticWheelScroller {
id: wheelScroller
anchors.fill: parent
parent: root
z: 12
flickable: root
}
}
+653
View File
@@ -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];
}
}
+107 -68
View File
@@ -1,6 +1,8 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtCanvas2D
import ZShell
Item { Item {
id: root id: root
@@ -36,54 +38,77 @@ Item {
property real fillAlpha: 0.15 property real fillAlpha: 0.15
property real lineWidth: 2 property real lineWidth: 2
property real markerDotRadius: 4 property real markerDotRadius: 4
property real labelFontSize: 10 property real labelFontSize: 8
Canvas { Canvas2D {
id: canvas id: canvas
anchors.fill: parent anchors.fill: parent
renderStrategy: Canvas.Immediate
alphaBlending: true
fillColor: "transparent"
onWidthChanged: requestPaint() onWidthChanged: requestPaint()
onHeightChanged: requestPaint() onHeightChanged: requestPaint()
function snapV(v) {
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);
if (root.curveSmoothing && pts.length > 2) { const dpr = ZUtils.dprFor(canvas);
const k = 1 / 6; for (let i = 0; i < pts.length - 1; i++) {
const n = pts.length; const p1 = pts[i];
const p2 = pts[i + 1];
const [c1x, c1y, c2x, c2y] = segmentControls(pts, i);
for (let i = 0; i < n - 1; i++) { const firstCol = Math.ceil(p1.x * dpr) + 1;
const p0 = pts[Math.max(i - 1, 0)]; const lastCol = Math.floor(p2.x * dpr) - 1;
const p1 = pts[i]; for (let col = firstCol; col <= lastCol; ++col) {
const p2 = pts[i + 1]; const x = col / dpr;
const p3 = pts[Math.min(i + 2, n - 1)]; ctx.lineTo(x, bezierY(p1, c1x, c1y, c2x, c2y, p2, x));
let c1x = p1.x + (p2.x - p0.x) * k;
let c1y = p1.y + (p2.y - p0.y) * k;
let c2x = p2.x - (p3.x - p1.x) * k;
let c2y = p2.y - (p3.y - p1.y) * k;
const minY = Math.min(p1.y, p2.y);
const maxY = Math.max(p1.y, p2.y);
c1y = Math.max(minY, Math.min(maxY, c1y));
c2y = Math.max(minY, Math.min(maxY, c2y));
ctx.bezierCurveTo(c1x, c1y, c2x, c2y, p2.x, p2.y);
}
} else {
for (let i = 0; i < pts.length - 1; i++) {
const p0 = pts[i];
const p1 = pts[i + 1];
const cx = (p0.x + p1.x) * 0.5;
ctx.bezierCurveTo(cx, p0.y, cx, p1.y, p1.x, p1.y);
} }
ctx.lineTo(p2.x, p2.y);
} }
} }
// Control points for segment i (pts[i] -> pts[i+1]), matching strokeCurve function bezierY(p1, c1x, c1y, c2x, c2y, p2, targetX) {
let tlo = 0;
let thi = 1;
for (let it = 0; it < 16; it++) {
const tm = (tlo + thi) / 2;
const u = 1 - tm;
const x = u * u * u * p1.x + 3 * u * u * tm * c1x + 3 * u * tm * tm * c2x + tm * tm * tm * p2.x;
if (x < targetX)
tlo = tm;
else
thi = tm;
}
const t = (tlo + thi) / 2;
const u = 1 - t;
return u * u * u * p1.y + 3 * u * u * t * c1y + 3 * u * t * t * c2y + t * t * t * p2.y;
}
function segmentControls(pts, i) { function segmentControls(pts, i) {
const p0 = pts[Math.max(i - 1, 0)]; const p0 = pts[Math.max(i - 1, 0)];
const p1 = pts[i]; const p1 = pts[i];
@@ -109,7 +134,6 @@ Item {
return [cx, p1.y, cx, p2.y]; return [cx, p1.y, cx, p2.y];
} }
// Point on the curve at a given x (x is monotonic per segment)
function pointAtX(pts, targetX) { function pointAtX(pts, targetX) {
let j = pts.length - 2; let j = pts.length - 2;
for (let i = 0; i < pts.length - 1; i++) { for (let i = 0; i < pts.length - 1; i++) {
@@ -138,10 +162,16 @@ Item {
const t = (tlo + thi) / 2; const t = (tlo + thi) / 2;
const u = 1 - t; const u = 1 - t;
const y = u * u * u * p1.y + 3 * u * u * t * c1y + 3 * u * t * t * c2y + t * t * t * p2.y; const y = u * u * u * p1.y + 3 * u * u * t * c1y + 3 * u * t * t * c2y + t * t * t * p2.y;
return { x: targetX, y }; return {
x: targetX,
y
};
}
function cutPoint(pts, targetX) {
return pointAtX(pts, targetX);
} }
// Part of the curve left/right of a cut x, including the cut point
function cutLeft(pts, targetX) { function cutLeft(pts, targetX) {
if (targetX <= pts[0].x) if (targetX <= pts[0].x)
return []; return [];
@@ -158,7 +188,7 @@ Item {
const left = pts.slice(0, j + 1); const left = pts.slice(0, j + 1);
if (targetX > pts[j].x) if (targetX > pts[j].x)
left.push(pointAtX(pts, targetX)); left.push(cutPoint(pts, targetX));
return left; return left;
} }
@@ -184,17 +214,20 @@ Item {
onPaint: { onPaint: {
const ctx = getContext("2d"); const ctx = getContext("2d");
if (!ctx)
return;
ctx.reset(); ctx.reset();
ctx.clearRect(0, 0, width, height);
const W = width; const W = width;
const H = height; const H = height;
if (W < 2 || H < 2) if (W < 2 || H < 2)
return; return;
const padLeft = root.labelFontSize * 3.4; const padLeft = root.labelFontSize * 5;
const padRight = 2; const padRight = 0;
const padTop = 2; const padTop = 0;
const padBottom = root.labelFontSize + 8; const padBottom = root.labelFontSize + 14;
const plot = { const plot = {
x: padLeft, x: padLeft,
y: padTop, y: padTop,
@@ -204,8 +237,6 @@ Item {
if (plot.w < 2 || plot.h < 2) if (plot.w < 2 || plot.h < 2)
return; return;
// Auto-fit: NaN yMin/yMax derive from the data, snapped to the
// horizontal step grid, honoring minHorizontalLines
let lo = root.yMin; let lo = root.yMin;
let hi = root.yMax; let hi = root.yMax;
const loAuto = Number.isNaN(lo); const loAuto = Number.isNaN(lo);
@@ -214,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)
@@ -256,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;
@@ -279,11 +311,17 @@ Item {
const xAt = i => plot.x + (i / root.xSteps) * plot.w; const xAt = i => plot.x + (i / root.xSteps) * plot.w;
const yAt = v => plot.y + (1 - (v - scaleBottom) / range) * plot.h; const yAt = v => plot.y + (1 - (v - scaleBottom) / range) * plot.h;
const crisp = v => Math.floor(v) + 0.5;
// 1. Grid lines const dpr = ZUtils.dprFor(canvas);
const devW = w => Math.max(1, Math.round(w * dpr));
const snapLine = (v, w) => {
const dw = devW(w);
return (Math.round(v * dpr - dw / 2) + dw / 2) / dpr;
};
const gridW = devW(1) / dpr;
ctx.strokeStyle = Qt.alpha(root.gridColor, root.gridAlpha); ctx.strokeStyle = Qt.alpha(root.gridColor, root.gridAlpha);
ctx.lineWidth = 1; ctx.lineWidth = gridW;
if (root.verticalLineStep > 0) { if (root.verticalLineStep > 0) {
for (let i = 0; i <= root.xSteps; i += root.verticalLineStep) { for (let i = 0; i <= root.xSteps; i += root.verticalLineStep) {
@@ -292,7 +330,7 @@ Item {
if (i === root.xSteps && !root.lastXLine) if (i === root.xSteps && !root.lastXLine)
continue; continue;
const x = crisp(xAt(i)); const x = snapLine(xAt(i), 1);
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(x, plot.y); ctx.moveTo(x, plot.y);
ctx.lineTo(x, H); ctx.lineTo(x, H);
@@ -300,12 +338,11 @@ Item {
} }
} }
// Lines extend down into yPaddingBottom, snapped to the grid
const vStart = lo - (root.yPaddingBottom > 0 ? Math.floor(root.yPaddingBottom / root.horizontalLineStep) * root.horizontalLineStep : 0); const vStart = lo - (root.yPaddingBottom > 0 ? Math.floor(root.yPaddingBottom / root.horizontalLineStep) * root.horizontalLineStep : 0);
if (root.horizontalLineStep > 0) { if (root.horizontalLineStep > 0) {
for (let v = vStart; v <= hi + 0.001; v += root.horizontalLineStep) { for (let v = vStart; v <= hi + 0.001; v += root.horizontalLineStep) {
const y = crisp(yAt(v)); const y = snapLine(yAt(v), 1);
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(0, y); ctx.moveTo(0, y);
ctx.lineTo(plot.x + plot.w, y); ctx.lineTo(plot.x + plot.w, y);
@@ -313,9 +350,8 @@ Item {
} }
} }
// 2. Labels
ctx.fillStyle = Qt.alpha(root.labelColor, root.labelAlpha); ctx.fillStyle = Qt.alpha(root.labelColor, root.labelAlpha);
ctx.font = root.labelFontSize + "px sans-serif"; ctx.font = root.labelFontSize + "pt 'sans-serif'";
ctx.textAlign = "left"; ctx.textAlign = "left";
ctx.textBaseline = "top"; ctx.textBaseline = "top";
@@ -328,7 +364,7 @@ Item {
const text = root.xLabel(i); const text = root.xLabel(i);
if (text) if (text)
ctx.fillText(text, xAt(i) + 4, plot.y + plot.h + 4); ctx.fillText(text, xAt(i) + 4, plot.y + plot.h + 8);
} }
} }
@@ -341,7 +377,6 @@ Item {
} }
} }
// 3. Curve, split into segments at null gaps
const segments = []; const segments = [];
let current = null; let current = null;
for (let i = 0; i < series.length; i++) { for (let i = 0; i < series.length; i++) {
@@ -352,7 +387,7 @@ Item {
continue; continue;
} }
const pt = { const pt = {
x: xAt(i), x: snapV(xAt(i)),
y: yAt(series[i]) y: yAt(series[i])
}; };
if (!current) if (!current)
@@ -369,8 +404,6 @@ Item {
const markerPx = hasMarker ? xAt(root.markerX) : 0; const markerPx = hasMarker ? xAt(root.markerX) : 0;
let gapPx = 0; let gapPx = 0;
if (hasMarker) { if (hasMarker) {
// markerGap is measured from the dot edge, perpendicular to the
// curve; project onto x via the local curve slope
let slope = 0; let slope = 0;
const seg = segments.find(s => s[0].x <= markerPx && markerPx <= s[s.length - 1].x); const seg = segments.find(s => s[0].x <= markerPx && markerPx <= s[s.length - 1].x);
if (seg && seg.length > 2 && markerPx - 1 >= seg[0].x && markerPx + 1 <= seg[seg.length - 1].x) if (seg && seg.length > 2 && markerPx - 1 >= seg[0].x && markerPx + 1 <= seg[seg.length - 1].x)
@@ -382,7 +415,6 @@ Item {
if (seg.length < 2) if (seg.length < 2)
continue; continue;
// Fill stays continuous under the marker
ctx.fillStyle = Qt.alpha(root.lineColor, root.fillAlpha); ctx.fillStyle = Qt.alpha(root.lineColor, root.fillAlpha);
strokeCurve(ctx, seg); strokeCurve(ctx, seg);
ctx.lineTo(seg[seg.length - 1].x, plot.y + plot.h); ctx.lineTo(seg[seg.length - 1].x, plot.y + plot.h);
@@ -390,7 +422,6 @@ Item {
ctx.closePath(); ctx.closePath();
ctx.fill(); ctx.fill();
// Stroke has a gap around the marker
const pieces = hasMarker ? [cutLeft(seg, markerPx - gapPx), cutRight(seg, markerPx + gapPx)] : [seg]; const pieces = hasMarker ? [cutLeft(seg, markerPx - gapPx), cutRight(seg, markerPx + gapPx)] : [seg];
for (const pts of pieces) { for (const pts of pieces) {
@@ -398,24 +429,32 @@ Item {
continue; continue;
ctx.strokeStyle = root.lineColor; ctx.strokeStyle = root.lineColor;
ctx.lineWidth = root.lineWidth; ctx.lineWidth = ZUtils.snap(root.lineWidth, root);
strokeCurve(ctx, pts); strokeCurve(ctx, pts);
ctx.stroke(); ctx.stroke();
} }
} }
// 4. Marker: dashed vertical + dot
if (root.markerX >= 0 && root.markerX <= root.xSteps) { if (root.markerX >= 0 && root.markerX <= root.xSteps) {
const x = markerPx; const x = markerPx;
ctx.setLineDash([4, 4]);
ctx.strokeStyle = Qt.alpha(root.markerColor, 0.6); ctx.strokeStyle = Qt.alpha(root.markerColor, 0.6);
ctx.lineWidth = 1; ctx.lineWidth = gridW;
ctx.beginPath(); const lineX = snapLine(x, 1);
ctx.moveTo(crisp(x), plot.y); let dashY = plot.y;
ctx.lineTo(crisp(x), H); let dashOn = true;
ctx.stroke();
ctx.setLineDash([]); while (dashY < H) {
const dashEnd = Math.min(dashY + 4, H);
if (dashOn) {
ctx.beginPath();
ctx.moveTo(lineX, snapV(dashY));
ctx.lineTo(lineX, snapV(dashEnd));
ctx.stroke();
}
dashY = dashEnd;
dashOn = !dashOn;
}
const i0 = Math.floor(root.markerX); const i0 = Math.floor(root.markerX);
const v0 = series[i0]; const v0 = series[i0];
@@ -430,7 +469,7 @@ Item {
if (y != null) { if (y != null) {
ctx.fillStyle = root.markerColor; ctx.fillStyle = root.markerColor;
ctx.beginPath(); ctx.beginPath();
ctx.arc(x, y, root.markerDotRadius, 0, Math.PI * 2); ctx.circle(snapV(x), y, root.markerDotRadius);
ctx.fill(); ctx.fill();
} }
} }
+6 -2
View File
@@ -4,6 +4,7 @@ import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell import Quickshell
import ZShell.Config import ZShell.Config
import ZShell.Components
import qs.Drawers import qs.Drawers
import qs.Services import qs.Services
@@ -110,12 +111,14 @@ MouseArea {
color: Colors.palette.m3surfaceContainerLow color: Colors.palette.m3surfaceContainerLow
radius: parent.radius radius: parent.radius
ColumnLayout { SnapLayout {
id: column id: column
anchors.fill: parent anchors.fill: parent
anchors.margins: Tokens.padding.extraSmall anchors.margins: Tokens.padding.extraSmall
spacing: Tokens.spacing.extraSmall spacing: Tokens.spacing.extraSmall
orientation: Qt.Vertical
Repeater { Repeater {
id: repeater id: repeater
@@ -151,11 +154,12 @@ MouseArea {
} }
} }
RowLayout { SnapLayout {
id: menuOptionRow id: menuOptionRow
anchors.fill: parent anchors.fill: parent
anchors.margins: Tokens.padding.medium anchors.margins: Tokens.padding.medium
spacing: Tokens.spacing.small spacing: Tokens.spacing.small
MaterialIcon { MaterialIcon {
+120
View File
@@ -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)
}
}
}
}
+201
View File
@@ -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
}
}
+124
View File
@@ -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)
}
}
}
}
+156
View File
@@ -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 {}
}
}
}
+152
View File
@@ -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
}
}
+287
View File
@@ -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"
}
}
}
]
}
}
+70
View File
@@ -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 {}
}
}
+2 -1
View File
@@ -1,5 +1,6 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Components
import ZShell.Config import ZShell.Config
import qs.Services import qs.Services
@@ -53,7 +54,7 @@ Row {
onClicked: root.active?.clicked() onClicked: root.active?.clicked()
} }
RowLayout { SnapLayout {
id: textRow id: textRow
anchors.centerIn: parent anchors.centerIn: parent
+5 -2
View File
@@ -1,4 +1,5 @@
import ZShell import ZShell
import ZShell.Components
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import qs.Components import qs.Components
@@ -47,7 +48,7 @@ CustomRect {
z: -1 z: -1
} }
RowLayout { SnapLayout {
id: layout id: layout
anchors.fill: parent anchors.fill: parent
@@ -88,9 +89,11 @@ CustomRect {
} }
} }
ColumnLayout { SnapLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: 0 spacing: 0
orientation: Qt.Vertical
CustomText { CustomText {
id: title id: title
+17 -261
View File
@@ -4,14 +4,11 @@ pragma ComponentBehavior: Bound
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Services.Notifications import Quickshell.Services.Notifications
import Quickshell.Hyprland import ZShell.Services
import QtQuick
import ZShell
import qs.Components import qs.Components
import qs.Modules
import qs.Helpers import qs.Helpers
import qs.Services
import qs.Paths import qs.Paths
import qs.Services
import ZShell.Config import ZShell.Config
Singleton { Singleton {
@@ -19,11 +16,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();
}
}
} }
+7
View File
@@ -4,12 +4,16 @@ import Quickshell
import QtQuick import QtQuick
import ZShell.Internal import ZShell.Internal
import qs.Components import qs.Components
import qs.Services
Item { Item {
id: root id: root
readonly property alias content: contentLoader.item readonly property alias content: contentLoader.item
required property ScreenState screenState required property ScreenState screenState
property rect holeRect
property real holeRadius: 0
Loader { Loader {
id: contentLoader id: contentLoader
@@ -18,6 +22,9 @@ Item {
anchors.fill: parent anchors.fill: parent
sourceComponent: StrokeCanvas { sourceComponent: StrokeCanvas {
anchors.fill: parent
hole: root.holeRect
holeRadius: root.holeRadius
penColor: root.screenState.penColor penColor: root.screenState.penColor
penWidth: root.screenState.penWidth penWidth: root.screenState.penWidth
} }
+95
View File
@@ -0,0 +1,95 @@
import Quickshell
import QtQuick
import ZShell.Config
import qs.Components
CustomMouseArea {
id: root
required property real borderThickness
required property Drawing drawing
required property Panels panels
required property ScreenState screenState
required property EdgeGeometry geometry
function inLeftPanel(panel: Item, x: real, y: real): bool {
return x < panel.x + panel.width + geometry.insetLeft(borderThickness) && withinPanelHeight(panel, x, y);
}
function withinPanelHeight(panel: Item, x: real, y: real): bool {
const panelY = panel.y + geometry.insetTop(borderThickness);
return y >= panelY && y <= panelY + panel.height;
}
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: Qt.BlankCursor
enabled: z > 0
hoverEnabled: true
focus: root.screenState.isDrawing
z: 2
Binding {
target: root.drawing.content
property: "hoverPoint"
value: Qt.point(root.mouseX, root.mouseY)
}
Keys.onPressed: e => {
const mod = e.modifiers;
const key = e.key;
if ((key == Qt.Key_Z) && (mod & Qt.ControlModifier) && (mod & Qt.ShiftModifier)) {
root.drawing.content.redo();
e.accepted = true;
} else if ((key == Qt.Key_Z) && (mod & Qt.ControlModifier)) {
root.drawing.content.undoLastGroup();
e.accepted = true;
}
}
onPositionChanged: event => {
const x = event.x;
const y = event.y;
if (!drawing.content.hoverVisible)
drawing.content.showHover();
if (event.buttons & Qt.LeftButton) {
drawing.content.appendPoint(x, y);
return;
}
if (!(event.buttons & Qt.LeftButton) && inLeftPanel(panels.drawing, x, y)) {
z = -2;
panels.drawing.expand();
drawing.content.hideHover();
}
}
onPressed: event => {
const x = event.x;
const y = event.y;
panels.drawing.collapse();
if (event.buttons & Qt.LeftButton) {
panels.drawing.collapse();
drawing.content.beginStroke(x, y);
return;
}
if (event.button & Qt.RightButton) {
drawing.content.clear();
return;
}
}
onReleased: event => {
const x = event.x;
const y = event.y;
if (event.button & Qt.LeftButton) {
drawing.content.endStroke();
drawing.content.showHover();
}
}
}
+121 -198
View File
@@ -1,25 +1,33 @@
import Quickshell import Quickshell
import QtQuick import QtQuick
import ZShell.Config import ZShell.Config
import ZShell.Internal
import qs.Components import qs.Components
import qs.Helpers import qs.Helpers
import qs.Modules.Bar.Popouts import qs.Modules.Bar.Popouts
Item { CustomMouseArea {
id: root id: root
required property Drawing drawing
required property DrawingInput input
required property Item bar required property Item bar
required property real borderThickness required property real borderThickness
property bool dashboardShortcutActive
required property Drawing drawing
required property EdgeGeometry geometry required property EdgeGeometry geometry
property bool osdShortcutActive
required property Panels panels required property Panels panels
required property Wrapper popouts required property Wrapper popouts
required property ShellScreen screen required property ShellScreen screen
required property ScreenState screenState
property bool dashboardShortcutActive
property point dragStart
property bool osdShortcutActive
property bool setInitialPoint: false
property bool singleGestureTriggered: false property bool singleGestureTriggered: false
property bool utilitiesShortcutActive property bool utilitiesShortcutActive
required property ScreenState screenState
function inBottomPanel(panel: Item, x: real, y: real): bool { function inBottomPanel(panel: Item, x: real, y: real): bool {
const panelHeight = panel.height * (1 - (panel.offsetScale ?? 0)); const panelHeight = panel.height * (1 - (panel.offsetScale ?? 0));
@@ -56,124 +64,116 @@ Item {
return x >= panelX && x <= panelX + panel.width; return x >= panelX && x <= panelX + panel.width;
} }
acceptedButtons: bar.fullscreen ? Qt.NoButton : Qt.AllButtons
anchors.fill: parent anchors.fill: parent
hoverEnabled: true
DragHandler { onContainsMouseChanged: {
id: singleHandler if (!containsMouse) {
if (!osdShortcutActive) {
cursorShape: (active && centroid.pressPosition.y < root.bar.implicitHeight) ? Qt.ClosedHandCursor : undefined screenState.osd = false;
dragThreshold: 0 panels.osd.hovered = false;
enabled: !root.screenState.isDrawing
grabPermissions: PointerHandler.CanTakeOverFromHandlersOfSameType | PointerHandler.ApprovesTakeOverByAnything
maximumPointCount: 1
minimumPointCount: 1
target: null
onActiveChanged: {
if (!active)
root.singleGestureTriggered = false;
}
onCentroidChanged: {
const x = centroid.position.x;
const y = centroid.position.y;
const pressX = centroid.pressPosition.x;
const pressY = centroid.pressPosition.y;
const dragX = x - pressX;
const dragY = y - pressY;
if (pressY >= root.screen.height - Config.bar.border && pressX > root.screen.width / 5 && dragY < -Config.launcher.dragThreshold)
root.screenState.launcher = true;
if (root.singleGestureTriggered)
return;
const inDashboard = root.inTopPanel(root.panels.dashboardWrapper, pressX, pressY);
if (inDashboard && root.withinPanelWidth(root.panels.dashboardWrapper, x, y)) {
if (dragY > Config.dashboard.dragThreshold) {
root.screenState.dashboard = true;
root.singleGestureTriggered = true;
} else if (dragY < -Config.dashboard.dragThreshold) {
root.screenState.dashboard = false;
root.singleGestureTriggered = true;
}
} }
if (root.geometry.insetTop(root.borderThickness) > pressY && (Config.dashboard.showOnHover || !inDashboard)) { if (!popouts.currentName.startsWith("traymenu") || Config.bar.tray.showOnHover) {
if (dragY > 20) { popouts.hasCurrent = false;
root.screenState.settings = true;
root.singleGestureTriggered = true;
} else if (dragY < -20) {
root.screenState.settings = false;
root.singleGestureTriggered = true;
}
} }
if (pressY > root.screen.height - root.geometry.insetBottom(root.borderThickness) && pressX < root.screen.width / 5 && dragY < -50) if (Config.bar.autoHide || bar.fullscreen)
root.screenState.clipboard = true; bar.isHovered = false;
if (!Config.dock.hoverToReveal && pressY > root.screen.height - root.geometry.insetTop(root.borderThickness) && pressX > root.screen.width / 5 && !root.screenState.launcher) if (screenState.dashboard && Config.dashboard.showOnHover)
if (dragY < -10) { screenState.dashboard = false;
root.screenState.dock = true;
root.singleGestureTriggered = true;
}
if (pressX > root.screen.width - Config.bar.border && pressY < (root.screen.height / 2) && dragX < -Config.sidebar.dragThreshold) {
root.screenState.sidebar = true;
root.singleGestureTriggered = true;
}
if (pressX >= root.screen.width - Config.bar.border && pressY > (root.screen.height / 2) && dragX < -20) {
Hypr.dispatch(`hl.dsp.focus({ workspace = 'r+1', on_current_monitor = true })`);
root.singleGestureTriggered = true;
}
if (pressX <= Config.bar.border && dragX > 20) {
Hypr.dispatch(`hl.dsp.focus({ workspace = 'r-1', on_current_monitor = true })`);
root.singleGestureTriggered = true;
}
} }
} }
onPositionChanged: event => {
const x = event.x;
const y = event.y;
const dragX = x - dragStart.x;
const dragY = y - dragStart.y;
PointHandler { if (screenState.isDrawing && !inLeftPanel(panels.drawing, x, y)) {
id: drawingHandler input.z = 2;
panels.drawing.collapse();
}
property bool setInitialPoint: false if (root.bar.fullscreen && !root.bar.isHovered)
unhideTimer.start();
acceptedButtons: Qt.LeftButton | Qt.RightButton if (pressed && dragStart.y >= screen.height - Config.bar.border && dragStart.x > screen.width / 5 && dragY < -Config.launcher.dragThreshold)
enabled: root.screenState.isDrawing && (!root.inLeftPanel(root.panels.drawing, hoverHandler.point.position.x, hoverHandler.point.position.y) || !root.panels.drawing.expanded) screenState.launcher = true;
onActiveChanged: { const entryAt = bar.entryAt(geometry.axisPos(x, y));
if (!active) { const showDashboard = Config.dashboard.showOnHover && inTopPanel(panels.dashboardWrapper, x, y) && (!entryAt || entryAt === "activeWindow");
setInitialPoint = false;
root.drawing.content.endStroke(); if (!dashboardShortcutActive) {
} else { screenState.dashboard = showDashboard;
root.panels.drawing.collapse(); } else if (showDashboard) {
dashboardShortcutActive = false;
}
if (pressed && geometry.insetTop(borderThickness) > dragStart.y && withinPanelWidth(panels.dashboardWrapper, x, y)) {
if (dragY > Config.dashboard.dragThreshold)
screenState.dashboard = true;
else if (dragY < -Config.dashboard.dragThreshold)
screenState.dashboard = false;
}
if (pressed && geometry.insetTop(borderThickness) > dragStart.y && !inTopPanel(panels.dashboardWrapper, dragStart.x, dragStart.y)) {
if (dragY > 20) {
screenState.settings = true;
} else if (dragY < -20) {
screenState.settings = false;
} }
} }
onPointChanged: {
if (!active)
return;
const x = point.position.x;
const y = point.position.y;
const origX = point.pressPosition.x;
const origY = point.pressPosition.y;
if (point.pressedButtons & Qt.RightButton) { if (pressed && dragStart.y > screen.height - geometry.insetBottom(borderThickness) && dragStart.x < screen.width / 5 && dragY < -50)
root.drawing.content.clear(); screenState.clipboard = true;
return;
if (pressed && !Config.dock.hoverToReveal && dragStart.y > screen.height - geometry.insetTop(borderThickness) && dragStart.x > screen.width / 5 && !screenState.launcher)
if (dragY < -10)
screenState.dock = true;
if (pressed && dragStart.x > screen.width - Config.bar.border && dragStart.y < (screen.height / 2) && dragX < -Config.sidebar.dragThreshold)
screenState.sidebar = true;
if (pressed && dragStart.x >= screen.width - Config.bar.border && dragStart.y > (screen.height / 2) && dragX < -20)
Hypr.dispatch(`hl.dsp.focus({ workspace = 'r+1', on_current_monitor = true })`);
if (pressed && dragStart.x <= Config.bar.border && dragX > 20)
Hypr.dispatch(`hl.dsp.focus({ workspace = 'r-1', on_current_monitor = true })`);
if (!screenState.bar && Config.bar.autoHide && geometry.barContains(x, y, true))
bar.isHovered = true;
if (panels.sidebar.offsetScale === 1) {
const showOsd = inRightPanel(panels.osdWrapper, x, y);
if (showOsd) {
osdShortcutActive = false;
panels.osd.hovered = true;
} }
if (x === 0 && y === 0 && origX === 0 && origY === 0) } else {
return; const outOfSidebar = x < width - panels.sidebar.width * (1 - panels.sidebar.offsetScale);
const showOsd = outOfSidebar && inRightPanel(panels.osdWrapper, x, y);
if (!setInitialPoint) { if (!osdShortcutActive) {
setInitialPoint = true; screenState.osd = showOsd;
root.drawing.content.beginStroke(origX, origY); panels.osd.hovered = showOsd;
return; } else if (showOsd) {
osdShortcutActive = false;
panels.osd.hovered = true;
} }
root.drawing.content.appendPoint(x, y);
} }
if (Config.dock.enable && Config.dock.hoverToReveal && !screenState.dock && !screenState.launcher && inBottomPanel(panels.dock, x, y))
screenState.dock = true;
if (geometry.barContains(x, y))
bar.checkPopout(geometry.axisPos(x, y));
}
onPressed: event => {
dragStart = Qt.point(event.x, event.y);
} }
Timer { Timer {
@@ -184,98 +184,20 @@ Item {
running: false running: false
onTriggered: { onTriggered: {
if (hoverHandler.hovered) if (root.containsMouse)
root.bar.isHovered = true; root.bar.isHovered = true;
} }
} }
HoverHandler {
id: hoverHandler
cursorShape: root.screenState.isDrawing && !root.inLeftPanel(root.panels.drawing, point.position.x, point.position.y) ? Qt.BlankCursor : undefined
onHoveredChanged: {
if (!hovered) {
if (!root.osdShortcutActive) {
root.screenState.osd = false;
root.panels.osd.hovered = false;
}
if (!root.popouts.currentName.startsWith("traymenu") || Config.bar.tray.showOnHover) {
root.popouts.hasCurrent = false;
}
if (Config.bar.autoHide || root.bar.fullscreen)
root.bar.isHovered = false;
if (root.screenState.dashboard && Config.dashboard.showOnHover)
root.screenState.dashboard = false;
}
}
onPointChanged: {
const x = point.position.x;
const y = point.position.y;
if (root.screenState.isDrawing) {
if (root.inLeftPanel(root.panels.drawing, x, y) && !(drawingHandler.point.pressedButtons & Qt.LeftButton)) {
root.panels.drawing.expand();
root.drawing.content.hideHover();
return;
}
root.drawing.content.showHover(x, y);
return;
}
if (!root.screenState.bar && Config.bar.autoHide && root.geometry.barContains(x, y, true))
root.bar.isHovered = true;
if (root.bar.fullscreen && !root.bar.isHovered)
unhideTimer.start();
if (root.panels.sidebar.offsetScale === 1) {
const showOsd = root.inRightPanel(root.panels.osdWrapper, x, y);
if (showOsd) {
root.osdShortcutActive = false;
root.panels.osd.hovered = true;
}
} else {
const outOfSidebar = x < root.width - root.panels.sidebar.width * (1 - root.panels.sidebar.offsetScale);
const showOsd = outOfSidebar && root.inRightPanel(root.panels.osdWrapper, x, y);
if (!root.osdShortcutActive) {
root.screenState.osd = showOsd;
root.panels.osd.hovered = showOsd;
} else if (showOsd) {
root.osdShortcutActive = false;
root.panels.osd.hovered = true;
}
}
if (Config.dock.enable && Config.dock.hoverToReveal && !root.screenState.dock && !root.screenState.launcher && root.inBottomPanel(root.panels.dock, x, y))
root.screenState.dock = true;
const inDashboard = root.inTopPanel(root.panels.dashboardWrapper, x, y);
if (Config.dashboard.showOnHover)
root.screenState.dashboard = inDashboard;
if (root.geometry.barContains(x, y))
root.bar.checkPopout(root.geometry.axisPos(x, y));
}
}
Connections { Connections {
function onDashboardChanged() { function onDashboardChanged(): void {
if (root.screenState.dashboard) { if (root.screenState.dashboard) {
const inDashboardArea = root.inTopPanel(root.panels.dashboard, root.mouseX, root.mouseY); const inDashboardArea = root.inTopPanel(root.panels.dashboardWrapper, root.mouseX, root.mouseY);
if (!inDashboardArea) { if (!inDashboardArea) {
root.dashboardShortcutActive = true; root.dashboardShortcutActive = true;
} }
if (root.panels.launcher.x + root.panels.launcher.width > root.panels.dashboardWrapper.x)
root.screenState.launcher = false;
root.screenState.launcher = false;
root.screenState.settings = false; root.screenState.settings = false;
root.screenState.sidebar = false; root.screenState.sidebar = false;
root.screenState.resources = false; root.screenState.resources = false;
@@ -285,12 +207,15 @@ Item {
} }
} }
function onIsDrawingChanged() { function onIsDrawingChanged(): void {
if (!root.screenState.isDrawing) if (root.screenState.isDrawing) {
root.drawing.clear(); root.drawing.content.showHover();
} else {
root.drawing.content.clear();
}
} }
function onLauncherChanged() { function onLauncherChanged(): void {
if (!root.screenState.launcher) { if (!root.screenState.launcher) {
root.dashboardShortcutActive = false; root.dashboardShortcutActive = false;
root.osdShortcutActive = false; root.osdShortcutActive = false;
@@ -305,16 +230,13 @@ Item {
} }
if (root.screenState.launcher) { if (root.screenState.launcher) {
if (root.panels.dashboardWrapper.x < root.panels.launcher.x + root.panels.launcher.width) { root.screenState.dashboard = false;
root.screenState.dashboard = false;
}
root.screenState.dock = false; root.screenState.dock = false;
root.screenState.settings = false; root.screenState.settings = false;
} }
} }
function onOsdChanged() { function onOsdChanged(): void {
if (root.screenState.osd) { if (root.screenState.osd) {
const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY); const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY);
if (!inOsdArea) { if (!inOsdArea) {
@@ -325,7 +247,7 @@ Item {
} }
} }
function onResourcesChanged() { function onResourcesChanged(): void {
if (root.screenState.resources) { if (root.screenState.resources) {
if (root.popouts.currentName.startsWith("audio") || root.popouts.currentName.startsWith("updates")) if (root.popouts.currentName.startsWith("audio") || root.popouts.currentName.startsWith("updates"))
root.popouts.hasCurrent = false; root.popouts.hasCurrent = false;
@@ -335,7 +257,7 @@ Item {
} }
} }
function onSettingsChanged() { function onSettingsChanged(): void {
if (root.screenState.settings) { if (root.screenState.settings) {
root.screenState.resources = false; root.screenState.resources = false;
root.screenState.dashboard = false; root.screenState.dashboard = false;
@@ -345,9 +267,10 @@ Item {
} }
} }
function onSidebarChanged() { function onSidebarChanged(): void {
if (root.screenState.sidebar) { if (root.screenState.sidebar) {
root.screenState.dashboard = false; if (root.panels.dashboardWrapper.x + root.panels.dashboardWrapper.width > root.panels.sidebar.x)
root.screenState.dashboard = false;
root.screenState.settings = false; root.screenState.settings = false;
root.popouts.hasCurrent = false; root.popouts.hasCurrent = false;
} }
-315
View File
@@ -1,315 +0,0 @@
import Quickshell
import QtQuick
import ZShell.Config
import ZShell.Internal
import qs.Components
import qs.Helpers
import qs.Modules.Bar.Popouts
CustomMouseArea {
id: root
required property Item bar
required property real borderThickness
property bool dashboardShortcutActive
property point dragStart
required property Drawing drawing
required property EdgeGeometry geometry
property bool osdShortcutActive
required property Panels panels
required property Wrapper popouts
required property ShellScreen screen
required property ScreenState screenState
property bool setInitialPoint: false
property bool singleGestureTriggered: false
property bool utilitiesShortcutActive
function inBottomPanel(panel: Item, x: real, y: real): bool {
const panelHeight = panel.height * (1 - (panel.offsetScale ?? 0));
return y > root.height - (panelHeight + geometry.insetBottom(borderThickness)) && withinPanelWidth(panel, x, y);
}
function inLeftPanel(panel: Item, x: real, y: real): bool {
return x < panel.x + panel.width + geometry.insetLeft(borderThickness) && withinPanelHeight(panel, x, y);
}
function inPopoutArea(x: real, y: real): bool {
const panel = panels.popoutsWrapper;
if (!geometry.horizontal)
return inLeftPanel(panel, x, y);
return withinPanelWidth(panel, x, y) && withinPanelHeight(panel, x, y);
}
function inRightPanel(panel: Item, x: real, y: real): bool {
return x > panel.x + geometry.insetLeft(borderThickness) && withinPanelHeight(panel, x, y);
}
function inTopPanel(panel: Item, x: real, y: real): bool {
const panelHeight = panel.height * (1 - (panel.offsetScale ?? 0));
return y < geometry.insetTop(borderThickness) + panelHeight && withinPanelWidth(panel, x, y);
}
function withinPanelHeight(panel: Item, x: real, y: real): bool {
const panelY = panel.y + geometry.insetTop(borderThickness);
return y >= panelY && y <= panelY + panel.height;
}
function withinPanelWidth(panel: Item, x: real, y: real): bool {
const panelX = panel.x + geometry.insetLeft(borderThickness);
return x >= panelX && x <= panelX + panel.width;
}
acceptedButtons: bar.fullscreen ? Qt.NoButton : Qt.AllButtons
anchors.fill: parent
hoverEnabled: true
onContainsMouseChanged: {
if (!containsMouse) {
if (!osdShortcutActive) {
screenState.osd = false;
panels.osd.hovered = false;
}
if (!popouts.currentName.startsWith("traymenu") || Config.bar.tray.showOnHover) {
popouts.hasCurrent = false;
}
if (Config.bar.autoHide || bar.fullscreen)
bar.isHovered = false;
if (screenState.dashboard && Config.dashboard.showOnHover)
screenState.dashboard = false;
}
}
onPositionChanged: event => {
const x = event.x;
const y = event.y;
const dragX = x - dragStart.x;
const dragY = y - dragStart.y;
if (screenState.isDrawing) {
if (pressed) {
drawing.content.appendPoint(x, y);
} else {
if (inLeftPanel(panels.drawing, x, y)) {
if (!(pressedButtons & Qt.LeftButton)) {
panels.drawing.expand();
drawing.content.hideHover();
Cursor.clearOverride();
}
} else {
Cursor.setOverride(Qt.BlankCursor);
drawing.content.showHover(x, y);
}
}
if (drawing.content.hoverVisible)
drawing.content.showHover(x, y);
return;
}
if (root.bar.fullscreen && !root.bar.isHovered)
unhideTimer.start();
if (pressed && dragStart.y >= screen.height - Config.bar.border && dragStart.x > screen.width / 5 && dragY < -Config.launcher.dragThreshold)
screenState.launcher = true;
const entryAt = bar.entryAt(geometry.axisPos(x, y));
const showDashboard = Config.dashboard.showOnHover && inTopPanel(panels.dashboardWrapper, x, y) && (!entryAt || entryAt === "activeWindow");
if (!dashboardShortcutActive) {
screenState.dashboard = showDashboard;
} else if (showDashboard) {
dashboardShortcutActive = false;
}
if (pressed && geometry.insetTop(borderThickness) > dragStart.y && withinPanelWidth(panels.dashboardWrapper, x, y)) {
if (dragY > Config.dashboard.dragThreshold)
screenState.dashboard = true;
else if (dragY < -Config.dashboard.dragThreshold)
screenState.dashboard = false;
}
if (pressed && geometry.insetTop(borderThickness) > dragStart.y && !inTopPanel(panels.dashboardWrapper, dragStart.x, dragStart.y)) {
if (dragY > 20) {
screenState.settings = true;
} else if (dragY < -20) {
screenState.settings = false;
}
}
if (pressed && dragStart.y > screen.height - geometry.insetBottom(borderThickness) && dragStart.x < screen.width / 5 && dragY < -50)
screenState.clipboard = true;
if (pressed && !Config.dock.hoverToReveal && dragStart.y > screen.height - geometry.insetTop(borderThickness) && dragStart.x > screen.width / 5 && !screenState.launcher)
if (dragY < -10)
screenState.dock = true;
if (pressed && dragStart.x > screen.width - Config.bar.border && dragStart.y < (screen.height / 2) && dragX < -Config.sidebar.dragThreshold)
screenState.sidebar = true;
if (pressed && dragStart.x >= screen.width - Config.bar.border && dragStart.y > (screen.height / 2) && dragX < -20)
Hypr.dispatch(`hl.dsp.focus({ workspace = 'r+1', on_current_monitor = true })`);
if (pressed && dragStart.x <= Config.bar.border && dragX > 20)
Hypr.dispatch(`hl.dsp.focus({ workspace = 'r-1', on_current_monitor = true })`);
if (!screenState.bar && Config.bar.autoHide && geometry.barContains(x, y, true))
bar.isHovered = true;
if (panels.sidebar.offsetScale === 1) {
const showOsd = inRightPanel(panels.osdWrapper, x, y);
if (showOsd) {
osdShortcutActive = false;
panels.osd.hovered = true;
}
} else {
const outOfSidebar = x < width - panels.sidebar.width * (1 - panels.sidebar.offsetScale);
const showOsd = outOfSidebar && inRightPanel(panels.osdWrapper, x, y);
if (!osdShortcutActive) {
screenState.osd = showOsd;
panels.osd.hovered = showOsd;
} else if (showOsd) {
osdShortcutActive = false;
panels.osd.hovered = true;
}
}
if (Config.dock.enable && Config.dock.hoverToReveal && !screenState.dock && !screenState.launcher && inBottomPanel(panels.dock, x, y))
screenState.dock = true;
if (geometry.barContains(x, y))
bar.checkPopout(geometry.axisPos(x, y));
}
onPressed: event => {
dragStart = Qt.point(event.x, event.y);
if (screenState.isDrawing) {
if (event.button & Qt.RightButton) {
drawing.content.clear();
return;
}
if (!inLeftPanel(panels.drawing, mouseX, mouseY))
drawing.content.beginStroke(event.x, event.y);
}
}
onPressedChanged: {
if (!pressed) {
if (screenState.isDrawing) {
setInitialPoint = false;
drawing.content.endStroke();
if (!inLeftPanel(panels.drawing, mouseX, mouseY))
drawing.content.showHover(mouseX, mouseY);
}
} else {
if (screenState.isDrawing) {
panels.drawing.collapse();
}
}
}
Timer {
id: unhideTimer
interval: Config.bar.revealDelay
repeat: false
running: false
onTriggered: {
if (root.containsMouse)
root.bar.isHovered = true;
}
}
Connections {
function onDashboardChanged(): void {
if (root.screenState.dashboard) {
const inDashboardArea = root.inTopPanel(root.panels.dashboardWrapper, root.mouseX, root.mouseY);
if (!inDashboardArea) {
root.dashboardShortcutActive = true;
}
root.screenState.launcher = false;
root.screenState.settings = false;
root.screenState.sidebar = false;
root.screenState.resources = false;
root.popouts.hasCurrent = false;
} else {
root.dashboardShortcutActive = false;
}
}
function onIsDrawingChanged(): void {
if (root.screenState.isDrawing) {
Cursor.setOverride(Qt.BlankCursor);
} else {
Cursor.clearOverride();
}
}
function onLauncherChanged(): void {
if (!root.screenState.launcher) {
root.dashboardShortcutActive = false;
root.osdShortcutActive = false;
root.utilitiesShortcutActive = false;
const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY);
if (!inOsdArea) {
root.screenState.osd = false;
root.panels.osd.hovered = false;
}
}
if (root.screenState.launcher) {
root.screenState.dashboard = false;
root.screenState.dock = false;
root.screenState.settings = false;
}
}
function onOsdChanged(): void {
if (root.screenState.osd) {
const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY);
if (!inOsdArea) {
root.osdShortcutActive = true;
}
} else {
root.osdShortcutActive = false;
}
}
function onResourcesChanged(): void {
if (root.screenState.resources) {
if (root.popouts.currentName.startsWith("audio") || root.popouts.currentName.startsWith("updates"))
root.popouts.hasCurrent = false;
root.screenState.settings = false;
root.screenState.dashboard = false;
}
}
function onSettingsChanged(): void {
if (root.screenState.settings) {
root.screenState.resources = false;
root.screenState.dashboard = false;
root.screenState.sidebar = false;
root.panels.popouts.hasCurrent = false;
root.screenState.launcher = false;
}
}
function onSidebarChanged(): void {
if (root.screenState.sidebar) {
if (root.panels.dashboardWrapper.x + root.panels.dashboardWrapper.width > root.panels.sidebar.x)
root.screenState.dashboard = false;
root.screenState.settings = false;
root.popouts.hasCurrent = false;
}
}
target: root.screenState
}
}
+18 -8
View File
@@ -1,5 +1,7 @@
import Quickshell import Quickshell
import QtQuick import QtQuick
import ZShell.Config
import ZShell
import qs.Components import qs.Components
import qs.Modules.Bar.Popouts as Popouts import qs.Modules.Bar.Popouts as Popouts
import qs.Modules.Notifications as Notifications import qs.Modules.Notifications as Notifications
@@ -14,7 +16,6 @@ import qs.Modules.Settings as Settings
import qs.Modules.Drawing as Drawing import qs.Modules.Drawing as Drawing
import qs.Modules.Dock as Dock import qs.Modules.Dock as Dock
import qs.Modules.Clipboard as Clipboard import qs.Modules.Clipboard as Clipboard
import ZShell.Config
Item { Item {
id: root id: root
@@ -73,8 +74,11 @@ 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)
anchors.verticalCenter: parent.verticalCenter
y: ZUtils.snap((parent.height - implicitHeight) / 2, root)
// anchors.verticalCenter: parent.verticalCenter
clip: sidebar.visible clip: sidebar.visible
implicitHeight: osd.implicitHeight implicitHeight: osd.implicitHeight
implicitWidth: osd.implicitWidth * (1 - osd.offsetScale) implicitWidth: osd.implicitWidth * (1 - osd.offsetScale)
@@ -83,7 +87,7 @@ Item {
id: osd id: osd
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
screen: root.screen screen: root.screen
screenState: root.screenState screenState: root.screenState
sidebarOrSessionVisible: sidebar.visible sidebarOrSessionVisible: sidebar.visible
@@ -129,8 +133,9 @@ Item {
Launcher.Wrapper { Launcher.Wrapper {
id: launcher id: launcher
x: ZUtils.snap((parent.width - implicitWidth) / 2, root)
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
panels: root panels: root
screen: root.screen screen: root.screen
screenState: root.screenState screenState: root.screenState
@@ -150,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
@@ -190,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
View File
@@ -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
} }
} }
+6 -3
View File
@@ -3,11 +3,12 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Config import ZShell.Config
import ZShell.Components
import qs.Components import qs.Components
import qs.Helpers import qs.Helpers
import qs.Services import qs.Services
ColumnLayout { SnapLayout {
id: root id: root
readonly property real centerScale: Math.min(1, screenHeight / 1440) readonly property real centerScale: Math.min(1, screenHeight / 1440)
@@ -18,9 +19,11 @@ ColumnLayout {
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: false Layout.fillWidth: false
Layout.preferredWidth: centerWidth Layout.preferredWidth: centerWidth
orientation: Qt.Vertical
spacing: Tokens.spacing.largeIncreased * 2 spacing: Tokens.spacing.largeIncreased * 2
RowLayout { SnapLayout {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
spacing: Tokens.spacing.small spacing: Tokens.spacing.small
@@ -116,7 +119,7 @@ ColumnLayout {
} }
} }
RowLayout { SnapLayout {
id: input id: input
anchors.fill: parent anchors.fill: parent
+3 -2
View File
@@ -1,11 +1,12 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Config
import ZShell.Components
import qs.Components import qs.Components
import qs.Helpers import qs.Helpers
import ZShell.Config
import qs.Services import qs.Services
RowLayout { SnapLayout {
id: root id: root
required property var greeter required property var greeter
+1
View File
@@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound
import Quickshell import Quickshell
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 -4
View File
@@ -3,17 +3,20 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Config import ZShell.Config
import ZShell.Components
import qs.Components import qs.Components
import qs.Services import qs.Services
ColumnLayout { SnapLayout {
id: root id: root
required property var greeter required property var greeter
anchors.fill: parent anchors.fill: parent
anchors.margins: Tokens.padding.large anchors.margins: Tokens.padding.large
spacing: Tokens.spacing.large spacing: Tokens.spacing.large
orientation: Qt.Vertical
CustomText { CustomText {
Layout.fillWidth: true Layout.fillWidth: true
@@ -37,8 +40,9 @@ ColumnLayout {
duration: Tokens.anim.durations.extraLarge duration: Tokens.anim.durations.extraLarge
} }
} }
sourceComponent: ColumnLayout { sourceComponent: SnapLayout {
spacing: Tokens.spacing.largeIncreased spacing: Tokens.spacing.largeIncreased
orientation: Qt.Vertical
MaterialIcon { MaterialIcon {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
@@ -84,7 +88,7 @@ ColumnLayout {
} }
} }
RowLayout { SnapLayout {
id: row id: row
anchors.fill: parent anchors.fill: parent
@@ -97,9 +101,11 @@ ColumnLayout {
text: modelData.kind === "x11" ? "tv" : "desktop_windows" text: modelData.kind === "x11" ? "tv" : "desktop_windows"
} }
ColumnLayout { SnapLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: Tokens.spacing.small / 2 spacing: Tokens.spacing.small / 2
orientation: Qt.Vertical
CustomText { CustomText {
Layout.fillWidth: true Layout.fillWidth: true
+10 -4
View File
@@ -3,18 +3,21 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Config import ZShell.Config
import ZShell.Components
import qs.Components import qs.Components
import qs.Helpers import qs.Helpers
import qs.Services import qs.Services
ColumnLayout { SnapLayout {
id: root id: root
required property var greeter required property var greeter
anchors.fill: parent anchors.fill: parent
anchors.margins: Tokens.padding.large anchors.margins: Tokens.padding.large
spacing: Tokens.spacing.large spacing: Tokens.spacing.large
orientation: Qt.Vertical
CustomText { CustomText {
Layout.fillWidth: true Layout.fillWidth: true
@@ -38,8 +41,9 @@ ColumnLayout {
duration: Tokens.anim.durations.extraLarge duration: Tokens.anim.durations.extraLarge
} }
} }
sourceComponent: ColumnLayout { sourceComponent: SnapLayout {
spacing: Tokens.spacing.largeIncreased spacing: Tokens.spacing.largeIncreased
orientation: Qt.Vertical
MaterialIcon { MaterialIcon {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
@@ -86,7 +90,7 @@ ColumnLayout {
} }
} }
RowLayout { SnapLayout {
id: row id: row
anchors.fill: parent anchors.fill: parent
@@ -112,9 +116,11 @@ ColumnLayout {
} }
} }
ColumnLayout { SnapLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: Tokens.spacing.small / 2 spacing: Tokens.spacing.small / 2
orientation: Qt.Vertical
CustomText { CustomText {
Layout.fillWidth: true Layout.fillWidth: true
+1 -1
View File
@@ -53,7 +53,7 @@ CustomRect {
radius: Tokens.rounding.full radius: Tokens.rounding.full
width: row.implicitWidth + Tokens.padding.small * 2 width: row.implicitWidth + Tokens.padding.small * 2
RowLayout { SnapLayout {
id: row id: row
anchors.centerIn: parent anchors.centerIn: parent
+58 -8
View File
@@ -4,11 +4,13 @@ import Quickshell
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Config import ZShell.Config
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
GridLayout { SnapLayout {
id: root id: root
required property bool fullscreen required property bool fullscreen
@@ -22,7 +24,7 @@ GridLayout {
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 {
@@ -74,9 +76,47 @@ GridLayout {
return ch?.entryId ?? ""; return ch?.entryId ?? "";
} }
columnSpacing: Tokens.spacing.small readonly property var sectionRects: {
columns: horizontal ? -1 : 1 const count = repeater.count; // dependency: re-run when items are added/removed
rowSpacing: Tokens.spacing.small const names = ["start", "middle", "end"]; // constant ids by position
const bounds = [null];
let spacers = 0;
for (let i = 0; i < count; i++) {
const it = repeater.itemAt(i) as EntryWrapper;
if (!it)
continue;
if (it.entryId === "spacer") {
bounds.push(null);
spacers++;
continue;
}
const l = it.x, t = it.y, r = it.x + it.width, b = it.y + it.height;
const cur = bounds[spacers];
bounds[spacers] = cur ? {
l: Math.min(cur.l, l),
t: Math.min(cur.t, t),
r: Math.max(cur.r, r),
b: Math.max(cur.b, b)
} : {
l,
t,
r,
b
};
}
return bounds.map((c, n) => ({
id: names[n] ?? `section${n}`,
rect: c ? Qt.rect(c.l, c.t, c.r - c.l, c.b - c.t) : Qt.rect(0, 0, 0, 0)
}));
}
spacing: Tokens.spacing.small
orientation: horizontal ? Qt.Horizontal : Qt.Vertical
Repeater { Repeater {
id: repeater id: repeater
@@ -228,16 +268,26 @@ GridLayout {
} }
component EntryWrapper: Item { component EntryWrapper: Item {
readonly property int section: {
let n = 0;
for (let i = 0; i < index; i++) {
if (repeater.model.values[i].id === "spacer")
n++;
}
return n;
}
readonly property string entryId: modelData.id readonly property string entryId: modelData.id
required property int index required property int index
default property Item item
required property var modelData required property var modelData
default property Item item
Layout.alignment: root.horizontal ? Qt.AlignVCenter : Qt.AlignHCenter Layout.alignment: root.horizontal ? Qt.AlignVCenter : Qt.AlignHCenter
Layout.bottomMargin: !root.horizontal && index === repeater.count - 1 ? root.vPadding : 0 Layout.bottomMargin: !root.horizontal && index === repeater.count - 1 ? root.vPadding : 0
Layout.leftMargin: root.horizontal && index === 0 ? root.vPadding : (entryId === "statusIcons" && root.horizontal) ? -root.rowSpacing + Tokens.spacing.extraSmall : 0 Layout.leftMargin: root.horizontal && index === 0 ? root.vPadding : (entryId === "statusIcons" && root.horizontal) ? -root.spacing + Tokens.spacing.extraSmall : 0
Layout.rightMargin: root.horizontal && index === repeater.count - 1 ? root.vPadding : 0 Layout.rightMargin: root.horizontal && index === repeater.count - 1 ? root.vPadding : 0
Layout.topMargin: !root.horizontal && index === 0 ? root.vPadding : (entryId === "statusIcons" && !root.horizontal) ? -root.columnSpacing + Tokens.spacing.extraSmall : 0 Layout.topMargin: !root.horizontal && index === 0 ? root.vPadding : (entryId === "statusIcons" && !root.horizontal) ? -root.spacing + Tokens.spacing.extraSmall : 0
children: item children: item
implicitHeight: item?.implicitHeight ?? 0 implicitHeight: item?.implicitHeight ?? 0
implicitWidth: item?.implicitWidth ?? 0 implicitWidth: item?.implicitWidth ?? 0
+1
View File
@@ -9,6 +9,7 @@ import qs.Components
Item { Item {
id: root id: root
readonly property var sections: (content.item as Bar)?.sectionRects ?? []
readonly property int clampedExtent: Math.max(Config.bar.border, extent) readonly property int clampedExtent: Math.max(Config.bar.border, extent)
readonly property int contentThickness: Math.floor(Math.max(Config.bar.height, Tokens.sizes.bar.innerSize * (horizontal ? 1 : 1.5))) + padding * 2 readonly property int contentThickness: Math.floor(Math.max(Config.bar.height, Tokens.sizes.bar.innerSize * (horizontal ? 1 : 1.5))) + padding * 2
readonly property int exclusiveZone: Config.bar.autoHide ? Config.bar.border : contentThickness readonly property int exclusiveZone: Config.bar.autoHide ? Config.bar.border : contentThickness
+4 -2
View File
@@ -4,6 +4,7 @@ import Quickshell
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Config import ZShell.Config
import ZShell.Components
import qs.Modules.Bar.Popouts import qs.Modules.Bar.Popouts
import qs.Modules.Bar.Components.Common import qs.Modules.Bar.Components.Common
import qs.Helpers import qs.Helpers
@@ -43,7 +44,7 @@ WidgetBase {
timeTokens: timeTokens timeTokens: timeTokens
}; };
} }
required property GridLayout loader required property SnapLayout loader
required property Wrapper popouts required property Wrapper popouts
readonly property int roundedWidth: Math.round(timeText.implicitWidth) readonly property int roundedWidth: Math.round(timeText.implicitWidth)
readonly property int evenWidth: roundedWidth % 2 === 0 ? roundedWidth : roundedWidth + 1 readonly property int evenWidth: roundedWidth % 2 === 0 ? roundedWidth : roundedWidth + 1
@@ -69,7 +70,8 @@ WidgetBase {
} }
} }
ColumnLayout { SnapLayout {
orientation: Qt.Vertical
id: verticalColumn id: verticalColumn
anchors.centerIn: parent anchors.centerIn: parent
+1
View File
@@ -1,5 +1,6 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Components
import ZShell.Config import ZShell.Config
import qs.Components import qs.Components
import qs.Modules.Bar.Components.Common import qs.Modules.Bar.Components.Common
+2 -1
View File
@@ -1,6 +1,7 @@
import Quickshell import Quickshell
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Components
import qs.Components import qs.Components
import qs.Modules import qs.Modules
@@ -11,7 +12,7 @@ Item {
anchors.top: parent.top anchors.top: parent.top
implicitWidth: layout.implicitWidth implicitWidth: layout.implicitWidth
RowLayout { SnapLayout {
id: layout id: layout
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
+1
View File
@@ -1,5 +1,6 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Components
import Quickshell import Quickshell
import qs.Components import qs.Components
import ZShell.Config import ZShell.Config
+1
View File
@@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import Quickshell import Quickshell
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Components
import ZShell.Config import ZShell.Config
import ZShell.Services import ZShell.Services
import qs.Services import qs.Services
+4 -4
View File
@@ -4,6 +4,7 @@ import Quickshell
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Config import ZShell.Config
import ZShell.Components
import qs.Helpers import qs.Helpers
import qs.Modules.Bar.Components.StatusIcons import qs.Modules.Bar.Components.StatusIcons
import qs.Modules.Bar.Components.Common import qs.Modules.Bar.Components.Common
@@ -59,7 +60,7 @@ WidgetBase {
Anim {} Anim {}
} }
GridLayout { SnapLayout {
id: grid id: grid
anchors.bottomMargin: root.horizontal ? 0 : Tokens.padding.medium anchors.bottomMargin: root.horizontal ? 0 : Tokens.padding.medium
@@ -67,9 +68,8 @@ WidgetBase {
anchors.leftMargin: root.horizontal ? Tokens.padding.extraSmall : 0 anchors.leftMargin: root.horizontal ? Tokens.padding.extraSmall : 0
anchors.rightMargin: root.horizontal ? Tokens.padding.extraSmall : 0 anchors.rightMargin: root.horizontal ? Tokens.padding.extraSmall : 0
anchors.topMargin: root.horizontal ? 0 : Tokens.padding.extraSmall anchors.topMargin: root.horizontal ? 0 : Tokens.padding.extraSmall
columns: root.horizontal ? -1 : 1 orientation: root.horizontal ? Qt.Horizontal : Qt.Vertical
columnSpacing: root.spacing spacing: root.horizontal ? Tokens.spacing.extraSmall : -2
rowSpacing: root.spacing
Repeater { Repeater {
model: ScriptModel { model: ScriptModel {
@@ -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.Daemons import qs.Daemons
import qs.Components import qs.Components
@@ -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.Daemons import qs.Daemons
import qs.Components import qs.Components
@@ -1,6 +1,7 @@
import Quickshell.Services.UPower import Quickshell.Services.UPower
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Components
import ZShell.Config import ZShell.Config
import qs.Modules.Bar.Components.Common import qs.Modules.Bar.Components.Common
import qs.Components import qs.Components
+3 -2
View File
@@ -2,10 +2,11 @@ import QtQuick.Layouts
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Services.SystemTray import Quickshell.Services.SystemTray
import ZShell.Config
import ZShell.Components
import qs.Helpers import qs.Helpers
import qs.Modules.Bar.Popouts import qs.Modules.Bar.Popouts
import qs.Components import qs.Components
import ZShell.Config
import qs.Services import qs.Services
Item { Item {
@@ -15,7 +16,7 @@ Item {
readonly property real dpr: Hypr.monitorFor(loader?.screen).scale readonly property real dpr: Hypr.monitorFor(loader?.screen).scale
required property int ind required property int ind
required property SystemTrayItem item required property SystemTrayItem item
required property GridLayout loader required property SnapLayout loader
required property Wrapper popouts required property Wrapper popouts
function resolveIcon(app: string, icon: string): string { function resolveIcon(app: string, icon: string): string {
+5 -5
View File
@@ -4,6 +4,7 @@ import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell.Services.SystemTray import Quickshell.Services.SystemTray
import ZShell.Config import ZShell.Config
import ZShell.Components
import qs.Components import qs.Components
import qs.Modules.Bar.Components.Common import qs.Modules.Bar.Components.Common
import qs.Modules.Bar.Components.Tray import qs.Modules.Bar.Components.Tray
@@ -15,7 +16,7 @@ WidgetBase {
readonly property alias items: repeater readonly property alias items: repeater
readonly property alias layout: sysGrid readonly property alias layout: sysGrid
required property GridLayout loader required property SnapLayout loader
readonly property int padding: Tokens.padding.extraSmall readonly property int padding: Tokens.padding.extraSmall
required property Wrapper popouts required property Wrapper popouts
readonly property real size: horizontal ? sysGrid.implicitWidth + sysGrid.anchors.leftMargin + sysGrid.anchors.rightMargin : sysGrid.implicitHeight + sysGrid.anchors.topMargin + sysGrid.anchors.bottomMargin readonly property real size: horizontal ? sysGrid.implicitWidth + sysGrid.anchors.leftMargin + sysGrid.anchors.rightMargin : sysGrid.implicitHeight + sysGrid.anchors.topMargin + sysGrid.anchors.bottomMargin
@@ -28,7 +29,7 @@ WidgetBase {
radius: Tokens.rounding.full radius: Tokens.rounding.full
topRightRadius: horizontal ? Tokens.rounding.small / 2 : Tokens.rounding.full topRightRadius: horizontal ? Tokens.rounding.small / 2 : Tokens.rounding.full
GridLayout { SnapLayout {
id: sysGrid id: sysGrid
anchors.bottomMargin: root.horizontal ? 0 : Tokens.padding.extraSmall anchors.bottomMargin: root.horizontal ? 0 : Tokens.padding.extraSmall
@@ -36,9 +37,8 @@ WidgetBase {
anchors.leftMargin: root.horizontal ? Tokens.padding.extraSmall : 0 anchors.leftMargin: root.horizontal ? Tokens.padding.extraSmall : 0
anchors.rightMargin: root.horizontal ? Tokens.padding.extraSmall : 0 anchors.rightMargin: root.horizontal ? Tokens.padding.extraSmall : 0
anchors.topMargin: root.horizontal ? 0 : Tokens.padding.extraSmall anchors.topMargin: root.horizontal ? 0 : Tokens.padding.extraSmall
columns: root.horizontal ? -1 : 1 orientation: root.horizontal ? Qt.Horizontal : Qt.Vertical
rowSpacing: -2 spacing: root.horizontal ? Tokens.spacing.extraSmall : -2
columnSpacing: Tokens.spacing.extraSmall
Repeater { Repeater {
id: repeater id: repeater
+1
View File
@@ -1,5 +1,6 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Components
import ZShell.Config import ZShell.Config
import qs.Components import qs.Components
import qs.Modules.Bar.Components.Common import qs.Modules.Bar.Components.Common
+1 -1
View File
@@ -17,7 +17,7 @@ Item {
readonly property int maxSize: { readonly property int maxSize: {
const otherModules = bar.children.filter(c => c.enabled && c.entryId && c.item !== this && c.entryId !== "spacer"); const otherModules = bar.children.filter(c => c.enabled && c.entryId && c.item !== this && c.entryId !== "spacer");
const otherSize = otherModules.reduce((acc, curr) => acc + (horizontal ? (curr.item?.nonAnimWidth ?? curr.width ?? 0) : (curr.item.nonAnimHeight ?? curr.height ?? 0)), 0); const otherSize = otherModules.reduce((acc, curr) => acc + (horizontal ? (curr.item?.nonAnimWidth ?? curr.width ?? 0) : (curr.item.nonAnimHeight ?? curr.height ?? 0)), 0);
return horizontal ? (bar.width - otherSize - bar.columnSpacing * (bar.children.length - 1) - bar.vPadding * 2) : (bar.height - otherSize - bar.rowSpacing * (bar.children.length - 1) - bar.vPadding * 4); return horizontal ? (bar.width - otherSize - bar.spacing * (bar.children.length - 1) - bar.vPadding * 2) : (bar.height - otherSize - bar.spacing * (bar.children.length - 1) - bar.vPadding * 4);
} }
clip: true clip: true
+15 -10
View File
@@ -5,6 +5,7 @@ import Quickshell.Services.Pipewire
import Quickshell.Widgets import Quickshell.Widgets
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Components
import QtQuick.Controls import QtQuick.Controls
import ZShell.Config import ZShell.Config
import qs.Components import qs.Components
@@ -46,7 +47,8 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
component VolumesTab: ColumnLayout { component VolumesTab: SnapLayout {
orientation: Qt.Vertical
spacing: 12 spacing: 12
CustomRect { CustomRect {
@@ -56,14 +58,15 @@ Item {
color: Colors.tPalette.m3surfaceContainer color: Colors.tPalette.m3surfaceContainer
radius: root.rounding - vol.anchors.margins radius: root.rounding - vol.anchors.margins
ColumnLayout { SnapLayout {
orientation: Qt.Vertical
anchors.bottomMargin: Tokens.padding.small anchors.bottomMargin: Tokens.padding.small
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: Tokens.padding.small anchors.leftMargin: Tokens.padding.small
anchors.rightMargin: Tokens.padding.small anchors.rightMargin: Tokens.padding.small
anchors.topMargin: Tokens.padding.small anchors.topMargin: Tokens.padding.small
RowLayout { SnapLayout {
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
@@ -81,7 +84,7 @@ Item {
} }
} }
RowLayout { SnapLayout {
spacing: Tokens.spacing.largeIncreased spacing: Tokens.spacing.largeIncreased
CustomMouseArea { CustomMouseArea {
@@ -153,14 +156,15 @@ Item {
} }
} }
ColumnLayout { SnapLayout {
orientation: Qt.Vertical
anchors.bottomMargin: Tokens.padding.small anchors.bottomMargin: Tokens.padding.small
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: Tokens.padding.small anchors.leftMargin: Tokens.padding.small
anchors.rightMargin: Tokens.padding.small anchors.rightMargin: Tokens.padding.small
anchors.topMargin: Tokens.padding.small anchors.topMargin: Tokens.padding.small
RowLayout { SnapLayout {
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
@@ -178,7 +182,7 @@ Item {
} }
} }
RowLayout { SnapLayout {
spacing: Tokens.spacing.largeIncreased spacing: Tokens.spacing.largeIncreased
CustomMouseArea { CustomMouseArea {
@@ -276,14 +280,15 @@ Item {
text: Audio.getStreamName(appBox.modelData) text: Audio.getStreamName(appBox.modelData)
} }
ColumnLayout { SnapLayout {
orientation: Qt.Vertical
anchors.bottomMargin: Tokens.padding.small anchors.bottomMargin: Tokens.padding.small
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: Tokens.padding.small anchors.leftMargin: Tokens.padding.small
anchors.rightMargin: Tokens.padding.small anchors.rightMargin: Tokens.padding.small
anchors.topMargin: Tokens.padding.small anchors.topMargin: Tokens.padding.small
RowLayout { SnapLayout {
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
@@ -302,7 +307,7 @@ Item {
} }
} }
RowLayout { SnapLayout {
spacing: Tokens.spacing.largeIncreased spacing: Tokens.spacing.largeIncreased
CustomMouseArea { CustomMouseArea {
+4 -4
View File
@@ -4,6 +4,7 @@ import QtQuick
import Quickshell import Quickshell
import qs.Components import qs.Components
import ZShell.Config import ZShell.Config
import ZShell
Item { Item {
id: root id: root
@@ -36,7 +37,7 @@ Item {
const diff = parent.width - Math.floor(off + content.nonAnimWidth); const diff = parent.width - Math.floor(off + content.nonAnimWidth);
if (diff < 0) if (diff < 0)
return off + diff; return off + diff;
return Math.floor(Math.max(off, 0)); return ZUtils.snap(Math.max(off, 0), root);
} }
y: { y: {
if (horizontal) if (horizontal)
@@ -46,12 +47,11 @@ Item {
const diff = parent.height - Math.floor(off + content.nonAnimHeight); const diff = parent.height - Math.floor(off + content.nonAnimHeight);
if (diff < 0) if (diff < 0)
return off + diff; return off + diff;
return Math.max(off, 0); return ZUtils.snap(Math.max(off, 0), root);
} }
Behavior on offsetScale { Behavior on offsetScale {
Anim { Anim {}
}
} }
Behavior on x { Behavior on x {
enabled: !root.horizontal || root.offsetScale < 1 enabled: !root.horizontal || root.offsetScale < 1
+4 -3
View File
@@ -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
+3 -1
View File
@@ -4,6 +4,7 @@ import Quickshell
import Quickshell.Networking import Quickshell.Networking
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Components
import qs.Components import qs.Components
import ZShell.Config import ZShell.Config
import qs.Modules import qs.Modules
@@ -14,7 +15,8 @@ Item {
required property var wrapper required property var wrapper
ColumnLayout { SnapLayout {
orientation: Qt.Vertical
id: layout id: layout
spacing: 8 spacing: 8
+11 -16
View File
@@ -4,6 +4,7 @@ import Quickshell
import Quickshell.Widgets import Quickshell.Widgets
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Components
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Effects import QtQuick.Effects
import qs.Components import qs.Components
@@ -15,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
@@ -26,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 {
@@ -47,8 +43,9 @@ StackView {
duration: 0 duration: 0
} }
} }
component SubMenu: ColumnLayout { component SubMenu: SnapLayout {
id: menu id: menu
orientation: Qt.Vertical
required property QsMenuHandle handle required property QsMenuHandle handle
property bool isSubMenu property bool isSubMenu
@@ -59,12 +56,10 @@ StackView {
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 -1
View File
@@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Components
import Quickshell.Services.UPower import Quickshell.Services.UPower
import ZShell.Config import ZShell.Config
import qs.Modules.Bar.Components.Common import qs.Modules.Bar.Components.Common
@@ -35,7 +36,7 @@ Column {
Repeater { Repeater {
model: Battery.allPeripherals model: Battery.allPeripherals
RowLayout { SnapLayout {
id: layout id: layout
required property var modelData required property var modelData
+5 -3
View File
@@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Components
import Quickshell import Quickshell
import ZShell.Config import ZShell.Config
import qs.Components import qs.Components
@@ -86,7 +87,7 @@ CustomRect {
implicitWidth: 600 implicitWidth: 600
radius: root.radius - Tokens.padding.extraSmall radius: root.radius - Tokens.padding.extraSmall
RowLayout { SnapLayout {
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: Tokens.padding.small anchors.leftMargin: Tokens.padding.small
anchors.rightMargin: Tokens.padding.small anchors.rightMargin: Tokens.padding.small
@@ -96,7 +97,8 @@ CustomRect {
text: "package_2" text: "package_2"
} }
ColumnLayout { SnapLayout {
orientation: Qt.Vertical
Layout.fillWidth: true Layout.fillWidth: true
CustomText { CustomText {
@@ -114,7 +116,7 @@ CustomRect {
} }
} }
RowLayout { SnapLayout {
Layout.fillHeight: true Layout.fillHeight: true
Layout.preferredWidth: 300 Layout.preferredWidth: 300
+4 -2
View File
@@ -2,6 +2,7 @@ import Quickshell
import QtQuick import QtQuick
import qs.Components import qs.Components
import ZShell.Config import ZShell.Config
import ZShell
Item { Item {
id: root id: root
@@ -26,8 +27,8 @@ Item {
detachedMode = ""; detachedMode = "";
} }
implicitHeight: nonAnimHeight implicitHeight: ZUtils.snap(nonAnimHeight, root)
implicitWidth: nonAnimWidth implicitWidth: ZUtils.snap(nonAnimWidth, root)
Behavior on implicitHeight { Behavior on implicitHeight {
enabled: root.offsetScale < 1 enabled: root.offsetScale < 1
@@ -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
} }
+3 -2
View File
@@ -4,6 +4,7 @@ import Quickshell
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Components
import qs.Components import qs.Components
import qs.Helpers import qs.Helpers
import ZShell.Config import ZShell.Config
@@ -111,7 +112,7 @@ Item {
Repeater { Repeater {
model: lineRow.modelData.text.split("\t").slice(1) model: lineRow.modelData.text.split("\t").slice(1)
RowLayout { SnapLayout {
height: lineText.height height: lineText.height
width: lineText.height + Tokens.spacing.extraSmall width: lineText.height + Tokens.spacing.extraSmall
@@ -225,7 +226,7 @@ Item {
} }
} }
RowLayout { SnapLayout {
anchors.fill: parent anchors.fill: parent
spacing: Tokens.spacing.small spacing: Tokens.spacing.small
+5 -4
View File
@@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound
import Quickshell import Quickshell
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Components
import ZShell.Config import ZShell.Config
import ZShell import ZShell
import qs.Components import qs.Components
@@ -39,8 +40,8 @@ Item {
]; ];
return allTabs.filter(tab => tab.enabled); return allTabs.filter(tab => tab.enabled);
} }
readonly property real nonAnimHeight: tabs.implicitHeight + tabs.anchors.topMargin + view.implicitHeight + viewWrapper.anchors.margins + viewWrapper.anchors.topMargin readonly property real nonAnimHeight: ZUtils.snap(tabs.implicitHeight + tabs.anchors.topMargin + view.implicitHeight + viewWrapper.anchors.margins + viewWrapper.anchors.topMargin, root)
readonly property real nonAnimWidth: view.implicitWidth + viewWrapper.anchors.margins * 2 readonly property real nonAnimWidth: ZUtils.snap(view.implicitWidth + viewWrapper.anchors.margins * 2, root)
required property ScreenState screenState required property ScreenState screenState
implicitHeight: nonAnimHeight implicitHeight: nonAnimHeight
@@ -93,8 +94,8 @@ Item {
} }
anchors.fill: parent anchors.fill: parent
implicitHeight: currentItem?.implicitHeight ?? 0 implicitHeight: currentItem?.height ?? currentItem?.implicitHeight ?? 0
implicitWidth: currentItem?.implicitWidth ?? 0 implicitWidth: currentItem?.width ?? currentItem?.implicitWidth ?? 0
Repeater { Repeater {
id: repeater id: repeater
+55 -77
View File
@@ -1,104 +1,82 @@
import Quickshell import Quickshell
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell
import ZShell.Config
import ZShell.Components
import qs.Helpers import qs.Helpers
import qs.Components import qs.Components
import qs.Modules import qs.Modules
import ZShell.Config
import qs.Modules.Dashboard.Dash import qs.Modules.Dashboard.Dash
import qs.Services import qs.Services
GridLayout { SnapLayout {
orientation: Qt.Vertical
id: root id: root
readonly property bool dashboardVisible: screenState.dashboard readonly property bool dashboardVisible: screenState.dashboard
required property int radius required property int radius
required property ScreenState screenState required property ScreenState screenState
columnSpacing: Tokens.spacing.small spacing: ZUtils.snap(Tokens.spacing.small, root)
rowSpacing: Tokens.spacing.small
ParallelAnimation { height: ZUtils.snap(implicitHeight, root)
id: openAnim width: ZUtils.snap(implicitWidth, root)
Anim { SnapLayout {
property: "opacity"
target: root
to: 1
}
Anim {
property: "scale"
target: root
to: 1
}
}
ParallelAnimation {
id: closeAnim
Anim {
property: "opacity"
target: root
to: 0
}
Anim {
property: "scale"
target: root
to: 0.9
}
}
Rect {
Layout.column: 2
Layout.columnSpan: 3
Layout.preferredHeight: user.implicitHeight
Layout.preferredWidth: user.implicitWidth
radius: root.radius
User {
id: user
screenState: root.screenState
}
}
Rect {
Layout.columnSpan: 2
Layout.fillHeight: true
Layout.preferredWidth: Tokens.sizes.dashboard.weatherWidth
Layout.row: 0
radius: root.radius
Weather {}
}
Rect {
Layout.column: 0
Layout.columnSpan: 3
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: calendar.implicitHeight spacing: parent.spacing
Layout.row: 1
radius: root.radius
Calendar { Rect {
id: calendar id: weatherRect
Layout.fillHeight: true
Layout.preferredWidth: ZUtils.snap(Tokens.sizes.dashboard.weatherWidth, root)
radius: root.radius
screenState: root.screenState Weather {}
}
Rect {
id: userRect
Layout.preferredHeight: ZUtils.snap(user.implicitHeight, root)
Layout.preferredWidth: ZUtils.snap(user.implicitWidth, root)
radius: root.radius
User {
id: user
screenState: root.screenState
}
} }
} }
Rect { SnapLayout {
Layout.column: 3 Layout.fillWidth: true
Layout.columnSpan: 2 spacing: parent.spacing
Layout.fillHeight: true
Layout.preferredWidth: resources.implicitWidth
Layout.row: 1
radius: root.radius
Resources { Rect {
id: resources id: calendarRect
Layout.fillWidth: true
Layout.fillHeight: true
implicitHeight: ZUtils.snap(calendar.implicitHeight, root)
radius: root.radius
Calendar {
id: calendar
screenState: root.screenState
}
}
Rect {
id: resourcesRect
Layout.fillHeight: true
implicitWidth: ZUtils.snap(resources.implicitWidth, root)
radius: root.radius
Resources {
id: resources
}
} }
} }
+4 -2
View File
@@ -4,6 +4,7 @@ import Quickshell
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Components
import qs.Components import qs.Components
import ZShell.Config import ZShell.Config
import qs.Services import qs.Services
@@ -117,14 +118,15 @@ CustomMouseArea {
} }
} }
ColumnLayout { SnapLayout {
orientation: Qt.Vertical
id: inner id: inner
anchors.fill: parent anchors.fill: parent
anchors.margins: Tokens.padding.large anchors.margins: Tokens.padding.large
spacing: Tokens.spacing.extraSmall spacing: Tokens.spacing.extraSmall
RowLayout { SnapLayout {
id: monthNavigationRow id: monthNavigationRow
Layout.fillWidth: true Layout.fillWidth: true
+11 -9
View File
@@ -1,5 +1,7 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Components
import ZShell
import ZShell.Services import ZShell.Services
import ZShell.Config import ZShell.Config
import qs.Components import qs.Components
@@ -8,9 +10,8 @@ import qs.Services
Item { Item {
id: root id: root
anchors.bottom: parent.bottom anchors.fill: parent
anchors.top: parent.top implicitWidth: ZUtils.snap(layout.implicitWidth + layout.anchors.margins * 2, root)
implicitWidth: layout.implicitWidth + layout.anchors.margins * 4
ServiceRef { ServiceRef {
service: Storage service: Storage
@@ -28,13 +29,16 @@ Item {
service: Gpu service: Gpu
} }
ColumnLayout { SnapLayout {
id: layout id: layout
orientation: Qt.Vertical
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.margins: Tokens.padding.large anchors.margins: Tokens.padding.large
anchors.top: parent.top anchors.top: parent.top
x: ZUtils.snap((root.width - implicitWidth) / 2, root)
spacing: Tokens.spacing.medium spacing: Tokens.spacing.medium
Resource { Resource {
@@ -73,12 +77,10 @@ Item {
required property string icon required property string icon
Layout.fillHeight: true implicitSize: (root.height - layout.anchors.margins * 2 - layout.spacing * 4) / 5
implicitSize: height
Behavior on clampedVal { Behavior on clampedVal {
Anim { Anim {}
}
} }
MaterialIcon { MaterialIcon {
+9 -5
View File
@@ -1,6 +1,8 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Components
import M3Shapes import M3Shapes
import ZShell
import ZShell.Config import ZShell.Config
import qs.Modules.Dashboard.Media import qs.Modules.Dashboard.Media
import qs.Components import qs.Components
@@ -13,8 +15,8 @@ Item {
property ScreenState screenState property ScreenState screenState
required property int radius required property int radius
implicitWidth: 1000 implicitWidth: ZUtils.snap(1000, root)
implicitHeight: 320 implicitHeight: ZUtils.snap(320, root)
ClipRect { ClipRect {
anchors.fill: parent anchors.fill: parent
@@ -25,7 +27,7 @@ Item {
} }
} }
RowLayout { SnapLayout {
anchors.fill: parent anchors.fill: parent
anchors.margins: Tokens.padding.large anchors.margins: Tokens.padding.large
spacing: Tokens.spacing.extraLarge spacing: Tokens.spacing.extraLarge
@@ -94,7 +96,8 @@ Item {
active: opacity > 0 active: opacity > 0
opacity: 0 opacity: 0
sourceComponent: ColumnLayout { sourceComponent: SnapLayout {
orientation: Qt.Vertical
spacing: Tokens.spacing.small spacing: Tokens.spacing.small
MaterialShape { MaterialShape {
@@ -140,11 +143,12 @@ Item {
asynchronous: true asynchronous: true
active: opacity > 0 active: opacity > 0
sourceComponent: RowLayout { sourceComponent: SnapLayout {
spacing: Tokens.spacing.extraLarge spacing: Tokens.spacing.extraLarge
Details { Details {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true
} }
LyricsAndSelector { LyricsAndSelector {
+178 -154
View File
@@ -1,13 +1,14 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell.Services.Mpris import Quickshell.Services.Mpris
import ZShell
import ZShell.Components import ZShell.Components
import ZShell.Config import ZShell.Config
import qs.Components import qs.Components
import qs.Services import qs.Services
import qs.Helpers import qs.Helpers
ColumnLayout { Item {
id: root id: root
readonly property bool hasUnknownLength: (Players.active?.length ?? 0) > 2147483647 readonly property bool hasUnknownLength: (Players.active?.length ?? 0) > 2147483647
@@ -25,46 +26,187 @@ ColumnLayout {
return `${mins}:${secs}`; return `${mins}:${secs}`;
} }
spacing: Tokens.spacing.extraSmall implicitHeight: ZUtils.snap(layout.implicitHeight, root)
implicitWidth: layout.implicitWidth
Timer { SnapLayout {
running: Players.active?.isPlaying ?? false orientation: Qt.Vertical
interval: Config.dashboard.mediaUpdateInterval id: layout
triggeredOnStart: true anchors.left: parent.left
repeat: true anchors.right: parent.right
onTriggered: Players.active?.positionChanged() y: ZUtils.snap((parent.height - implicitHeight) / 2, root)
}
CustomText { spacing: ZUtils.snap(Tokens.spacing.extraSmall, root)
Layout.fillWidth: true
text: Players.active?.trackTitle ?? ""
font: Tokens.font.title.large
elide: Text.ElideRight
animate: true
}
CustomText { Timer {
Layout.fillWidth: true running: Players.active?.isPlaying ?? false
text: Players.active?.trackArtist || qsTr("Unknown artist") interval: Config.dashboard.mediaUpdateInterval
color: Colors.palette.m3onSurfaceVariant triggeredOnStart: true
font: Tokens.font.title.medium repeat: true
elide: Text.ElideRight onTriggered: Players.active?.positionChanged()
animate: true }
}
CustomText { CustomText {
Layout.fillWidth: true id: title
text: Players.active?.trackAlbum || qsTr("Unknown album") Layout.fillWidth: true
color: Colors.palette.m3secondary Layout.preferredHeight: ZUtils.snapCeil(implicitHeight, root)
font: Tokens.font.title.medium text: Players.active?.trackTitle ?? ""
elide: Text.ElideRight font: Tokens.font.title.large
animate: true elide: Text.ElideRight
} animate: true
}
RowLayout { CustomText {
Layout.topMargin: Tokens.spacing.extraLargeIncreased id: artist
Layout.fillWidth: true Layout.fillWidth: true
spacing: Tokens.spacing.small Layout.preferredHeight: ZUtils.snapCeil(implicitHeight, root)
text: Players.active?.trackArtist || qsTr("Unknown artist")
color: Colors.palette.m3onSurfaceVariant
font: Tokens.font.title.medium
elide: Text.ElideRight
animate: true
}
CustomText {
id: album
Layout.fillWidth: true
Layout.preferredHeight: ZUtils.snapCeil(implicitHeight, root)
text: Players.active?.trackAlbum || qsTr("Unknown album")
color: Colors.palette.m3secondary
font: Tokens.font.title.medium
elide: Text.ElideRight
animate: true
}
SnapLayout {
id: sliderLayout
Layout.topMargin: Tokens.spacing.extraLargeIncreased
Layout.fillWidth: true
spacing: ZUtils.snap(Tokens.spacing.small, root)
CustomText {
id: positionLabel
Layout.preferredWidth: ZUtils.snapCeil(timeMetrics.width, root)
Layout.preferredHeight: positionSlider.implicitHeight
text: root.lengthStr(Players.active?.position ?? -1)
color: Colors.palette.m3onSurfaceVariant
font: timeMetrics.font
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
CustomSlider {
id: positionSlider
Layout.fillWidth: true
Layout.preferredHeight: ZUtils.snap(implicitHeight, root)
value: Players.active ? Players.active.position / (Players.active.length || 1) : 0
enabled: (Players.active?.canSeek ?? false) && !root.hasUnknownLength
wavy: true
animateWave: Players.active?.isPlaying ?? false
waveFrequency: 5
waveDuration: 2000
interactionOnMove: false
onInteraction: value => {
const active = Players.active;
if (active?.canSeek && active?.positionSupported)
active.position = value * active.length;
}
Binding {
target: positionLabel
property: "text"
value: root.lengthStr(positionSlider.pos * (Players.active?.length ?? 0))
when: positionSlider.dragging
}
}
CustomText {
Layout.preferredWidth: ZUtils.snapCeil(timeMetrics.width, root)
Layout.preferredHeight: positionSlider.implicitHeight
text: root.hasUnknownLength ? "--:--" : root.lengthStr(Players.active?.length ?? -1)
color: Colors.palette.m3onSurfaceVariant
font: timeMetrics.font
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
ButtonRow {
Layout.topMargin: Tokens.spacing.largeIncreased
Layout.fillWidth: true
spacing: Tokens.spacing.extraSmall
IconButton {
type: IconButton.Tonal
icon: "shuffle"
isRound: true
shapeMorph: true
checked: Players.active?.shuffle ?? false
font: Tokens.font.icon.builders.medium.weight(Font.Medium).build()
enabled: Players.active?.shuffleSupported
onClicked: Players.active.shuffle = !Players.active?.shuffle
implicitWidth: Math.round(implicitHeight * 0.9)
}
IconButton {
id: previousBtn
type: IconButton.Tonal
icon: "skip_previous"
isRound: true
shapeMorph: true
font: Tokens.font.icon.large
enabled: Players.active?.canGoPrevious
onClicked: Players.active?.previous()
}
IconButton {
id: playPauseBtn
icon: Players.active?.isPlaying ? "pause" : "play_arrow"
isRound: true
shapeMorph: true
fillWidth: true
checked: Players.active?.isPlaying ?? false
font: Tokens.font.icon.large
enabled: Players.active?.canTogglePlaying
onClicked: Players.active?.togglePlaying()
}
IconButton {
id: nextBtn
type: IconButton.Tonal
icon: "skip_next"
isRound: true
shapeMorph: true
font: Tokens.font.icon.large
enabled: Players.active?.canGoNext
onClicked: Players.active?.next()
}
IconButton {
type: IconButton.Tonal
icon: Players.active?.loopState === MprisLoopState.Track ? "repeat_one" : "repeat"
isRound: true
shapeMorph: true
checked: Players.active?.loopState === MprisLoopState.Track || Players.active?.loopState === MprisLoopState.Playlist
font: Tokens.font.icon.builders.medium.weight(Font.Medium).build()
enabled: Players.active?.loopSupported
onClicked: {
const state = Players.active.loopState;
if (state === MprisLoopState.None)
Players.active.loopState = MprisLoopState.Track;
else if (state === MprisLoopState.Track)
Players.active.loopState = MprisLoopState.Playlist;
else
Players.active.loopState = MprisLoopState.None;
}
implicitWidth: Math.round(implicitHeight * 0.9)
}
}
TextMetrics { TextMetrics {
id: timeMetrics id: timeMetrics
@@ -72,123 +214,5 @@ ColumnLayout {
text: Players.active ? root.lengthStr(Math.max(Players.active.position, root.hasUnknownLength ? 0 : Players.active.length)).replace(/[1-9]/g, "0") : "00:00" text: Players.active ? root.lengthStr(Math.max(Players.active.position, root.hasUnknownLength ? 0 : Players.active.length)).replace(/[1-9]/g, "0") : "00:00"
font: Tokens.font.label.medium font: Tokens.font.label.medium
} }
CustomText {
id: positionLabel
Layout.preferredWidth: timeMetrics.width
text: root.lengthStr(Players.active?.position ?? -1)
color: Colors.palette.m3onSurfaceVariant
font: timeMetrics.font
horizontalAlignment: Text.AlignHCenter
}
CustomSlider {
id: positionSlider
Layout.fillWidth: true
value: Players.active ? Players.active.position / (Players.active.length || 1) : 0
enabled: (Players.active?.canSeek ?? false) && !root.hasUnknownLength
wavy: true
animateWave: Players.active?.isPlaying ?? false
waveFrequency: 5
waveDuration: 2000
interactionOnMove: false
onInteraction: value => {
const active = Players.active;
if (active?.canSeek && active?.positionSupported)
active.position = value * active.length;
}
Binding {
target: positionLabel
property: "text"
value: root.lengthStr(positionSlider.pos * (Players.active?.length ?? 0))
when: positionSlider.dragging
}
}
CustomText {
Layout.preferredWidth: timeMetrics.width
text: root.hasUnknownLength ? "--:--" : root.lengthStr(Players.active?.length ?? -1)
color: Colors.palette.m3onSurfaceVariant
font: timeMetrics.font
horizontalAlignment: Text.AlignHCenter
}
}
ButtonRow {
Layout.topMargin: Tokens.spacing.largeIncreased
Layout.fillWidth: true
spacing: Tokens.spacing.extraSmall
IconButton {
type: IconButton.Tonal
icon: "shuffle"
isRound: true
shapeMorph: true
checked: Players.active?.shuffle ?? false
font: Tokens.font.icon.builders.medium.weight(Font.Medium).build()
enabled: Players.active?.shuffleSupported
onClicked: Players.active.shuffle = !Players.active?.shuffle
implicitWidth: Math.round(implicitHeight * 0.9)
}
IconButton {
id: previousBtn
type: IconButton.Tonal
icon: "skip_previous"
isRound: true
shapeMorph: true
font: Tokens.font.icon.large
enabled: Players.active?.canGoPrevious
onClicked: Players.active?.previous()
}
IconButton {
id: playPauseBtn
icon: Players.active?.isPlaying ? "pause" : "play_arrow"
isRound: true
shapeMorph: true
fillWidth: true
checked: Players.active?.isPlaying ?? false
font: Tokens.font.icon.large
enabled: Players.active?.canTogglePlaying
onClicked: Players.active?.togglePlaying()
}
IconButton {
id: nextBtn
type: IconButton.Tonal
icon: "skip_next"
isRound: true
shapeMorph: true
font: Tokens.font.icon.large
enabled: Players.active?.canGoNext
onClicked: Players.active?.next()
}
IconButton {
type: IconButton.Tonal
icon: Players.active?.loopState === MprisLoopState.Track ? "repeat_one" : "repeat"
isRound: true
shapeMorph: true
checked: Players.active?.loopState === MprisLoopState.Track || Players.active?.loopState === MprisLoopState.Playlist
font: Tokens.font.icon.builders.medium.weight(Font.Medium).build()
enabled: Players.active?.loopSupported
onClicked: {
const state = Players.active.loopState;
if (state === MprisLoopState.None)
Players.active.loopState = MprisLoopState.Track;
else if (state === MprisLoopState.Track)
Players.active.loopState = MprisLoopState.Playlist;
else
Players.active.loopState = MprisLoopState.None;
}
implicitWidth: Math.round(implicitHeight * 0.9)
}
} }
} }
+5 -2
View File
@@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Effects import QtQuick.Effects
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Components
import ZShell.Config import ZShell.Config
import ZShell.Services import ZShell.Services
import qs.Components import qs.Components
@@ -157,7 +158,8 @@ Item {
active: opacity > 0 active: opacity > 0
opacity: 0 opacity: 0
sourceComponent: ColumnLayout { sourceComponent: SnapLayout {
orientation: Qt.Vertical
spacing: Tokens.spacing.large spacing: Tokens.spacing.large
CustomRect { CustomRect {
@@ -198,7 +200,8 @@ Item {
active: opacity > 0 active: opacity > 0
opacity: 0 opacity: 0
sourceComponent: ColumnLayout { sourceComponent: SnapLayout {
orientation: Qt.Vertical
spacing: Tokens.spacing.small spacing: Tokens.spacing.small
MaterialIcon { MaterialIcon {
@@ -1,5 +1,6 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Components
import Quickshell import Quickshell
import Quickshell.Services.Mpris import Quickshell.Services.Mpris
import ZShell.Config import ZShell.Config
@@ -8,14 +9,15 @@ import qs.Helpers
import qs.Services import qs.Services
Item { Item {
ColumnLayout { SnapLayout {
orientation: Qt.Vertical
id: layout id: layout
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: Tokens.padding.medium anchors.leftMargin: Tokens.padding.medium
spacing: Tokens.spacing.medium spacing: Tokens.spacing.medium
RowLayout { SnapLayout {
Layout.bottomMargin: -Tokens.spacing.medium Layout.bottomMargin: -Tokens.spacing.medium
spacing: Tokens.spacing.medium spacing: Tokens.spacing.medium
z: 1 z: 1
+6 -4
View File
@@ -1,5 +1,6 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Components
import ZShell import ZShell
import ZShell.Blobs import ZShell.Blobs
import ZShell.Config import ZShell.Config
@@ -16,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
@@ -28,7 +29,7 @@ Item {
} }
} }
BlobRect { BlobRect2 {
id: btnRect id: btnRect
anchors.fill: parent anchors.fill: parent
@@ -41,7 +42,7 @@ Item {
} }
} }
BlobRect { BlobRect2 {
id: rect id: rect
anchors.right: parent.right anchors.right: parent.right
@@ -141,8 +142,9 @@ Item {
} }
] ]
ColumnLayout { SnapLayout {
id: layout id: layout
orientation: Qt.Vertical
anchors.centerIn: parent anchors.centerIn: parent
spacing: Tokens.spacing.extraSmall spacing: Tokens.spacing.extraSmall
@@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Components
import Quickshell import Quickshell
import ZShell.Config import ZShell.Config
import ZShell.Internal import ZShell.Internal
@@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Components
import Quickshell import Quickshell
import Quickshell.Widgets import Quickshell.Widgets
import ZShell.Config import ZShell.Config
@@ -38,7 +39,7 @@ CustomListView {
id: rowHover id: rowHover
} }
RowLayout { SnapLayout {
id: rowLayout id: rowLayout
anchors.left: parent.left anchors.left: parent.left
+6 -4
View File
@@ -23,7 +23,7 @@ Item {
service: Memory service: Memory
} }
RowLayout { SnapLayout {
id: layout id: layout
anchors.fill: parent anchors.fill: parent
spacing: Tokens.spacing.medium spacing: Tokens.spacing.medium
@@ -60,13 +60,14 @@ Item {
Layout.fillHeight: true Layout.fillHeight: true
implicitHeight: mainLayout.implicitHeight + mainLayout.anchors.margins * 2 implicitHeight: mainLayout.implicitHeight + mainLayout.anchors.margins * 2
ColumnLayout { SnapLayout {
orientation: Qt.Vertical
id: mainLayout id: mainLayout
anchors.fill: parent anchors.fill: parent
anchors.margins: Tokens.padding.large anchors.margins: Tokens.padding.large
spacing: 0 spacing: 0
RowLayout { SnapLayout {
spacing: Tokens.spacing.small spacing: Tokens.spacing.small
MaterialIcon { MaterialIcon {
@@ -131,7 +132,8 @@ Item {
} }
} }
ColumnLayout { SnapLayout {
orientation: Qt.Vertical
id: infoColumn id: infoColumn
anchors.right: parent.right anchors.right: parent.right
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
+57 -32
View File
@@ -4,6 +4,8 @@ import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Templates import QtQuick.Templates
import Quickshell import Quickshell
import ZShell
import ZShell.Components
import ZShell.Config import ZShell.Config
import qs.Components import qs.Components
import qs.Services import qs.Services
@@ -16,9 +18,17 @@ Item {
required property var tabs required property var tabs
readonly property alias count: bar.count readonly property alias count: bar.count
readonly property int tabIndicatorSpacing: Tokens.spacing.small readonly property int tabIndicatorSpacing: ZUtils.snap(Tokens.spacing.small, root)
implicitHeight: bar.implicitHeight + bar.anchors.topMargin + indicator.implicitHeight + indicator.anchors.topMargin + separator.implicitHeight function snapLayoutHeight(v) {
const dpr = ZUtils.dprFor(root);
let h = Math.ceil(v);
while (Math.abs(h * dpr - Math.round(h * dpr)) > 1e-6)
++h;
return h;
}
implicitHeight: ZUtils.snap(bar.implicitHeight + bar.anchors.topMargin + indicator.implicitHeight + indicator.anchors.topMargin + separator.implicitHeight, root)
TabBar { TabBar {
id: bar id: bar
@@ -31,9 +41,11 @@ Item {
currentIndex: root.screenState.dashboardTab currentIndex: root.screenState.dashboardTab
onCurrentIndexChanged: root.screenState.dashboardTab = currentIndex onCurrentIndexChanged: root.screenState.dashboardTab = currentIndex
implicitHeight: contentHeight topPadding: 0
bottomPadding: 0
implicitHeight: implicitContentHeight
background: null background: null
contentItem: RowLayout { contentItem: SnapLayout {
spacing: 0 spacing: 0
Repeater { Repeater {
@@ -116,10 +128,13 @@ Item {
readonly property bool current: TabBar.tabBar.currentItem === this readonly property bool current: TabBar.tabBar.currentItem === this
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredWidth: 1 // Uniform width across all tabs Layout.fillHeight: true
Layout.preferredWidth: 1
implicitWidth: implicitContentWidth implicitWidth: implicitContentWidth
implicitHeight: implicitContentHeight implicitHeight: implicitContentHeight
background: null background: null
topPadding: 0
bottomPadding: 0
contentItem: CustomMouseArea { contentItem: CustomMouseArea {
id: mouse id: mouse
@@ -131,8 +146,8 @@ Item {
root.screenState.dashboardTab = Math.max(root.screenState.dashboardTab - 1, 0); root.screenState.dashboardTab = Math.max(root.screenState.dashboardTab - 1, 0);
} }
implicitWidth: Math.max(icon.width, label.width) implicitWidth: Math.max(icon.implicitWidth, label.implicitWidth)
implicitHeight: icon.height + label.height implicitHeight: snapLayout.implicitHeight
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
@@ -146,39 +161,49 @@ Item {
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
implicitHeight: parent.height + root.tabIndicatorSpacing * 2 anchors.alignWhenCentered: false
implicitHeight: ZUtils.snap(parent.height + Tokens.sizes.dashboard.tabIndicatorSpacing * 2, stateLayer)
radius: Tokens.rounding.medium radius: Tokens.rounding.medium
color: tab.current ? Colors.palette.m3primary : Colors.palette.m3onSurface color: tab.current ? Colors.palette.m3primary : Colors.palette.m3onSurface
onClicked: root.screenState.dashboardTab = tab.TabBar.index onClicked: {
} root.screenState.dashboardTab = tab.TabBar.index;
MaterialIcon {
id: icon
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: label.top
text: tab.iconName
color: tab.current ? Colors.palette.m3primary : Colors.palette.m3onSurfaceVariant
fill: tab.current ? 1 : 0
fontStyle: Tokens.font.icon.medium
Behavior on fill {
Anim {
type: Anim.DefaultEffects
}
} }
} }
CustomText { SnapLayout {
id: label id: snapLayout
anchors.fill: parent
spacing: 0
orientation: Qt.Vertical
anchors.horizontalCenter: parent.horizontalCenter MaterialIcon {
anchors.bottom: parent.bottom id: icon
text: tab.text text: tab.iconName
color: tab.current ? Colors.palette.m3primary : Colors.palette.m3onSurfaceVariant color: tab.current ? Colors.palette.m3primary : Colors.palette.m3onSurfaceVariant
fill: tab.current ? 1 : 0
fontStyle: Tokens.font.icon.medium
Layout.fillHeight: true
Layout.alignment: Qt.AlignHCenter
Behavior on fill {
Anim {
type: Anim.DefaultEffects
}
}
}
CustomText {
id: label
text: tab.text
color: tab.current ? Colors.palette.m3primary : Colors.palette.m3onSurfaceVariant
Layout.fillHeight: true
Layout.alignment: Qt.AlignHCenter
}
} }
} }
} }
+15 -9
View File
@@ -1,11 +1,13 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell
import ZShell.Components
import ZShell.Config import ZShell.Config
import qs.Components import qs.Components
import qs.Services import qs.Services
import qs.Helpers import qs.Helpers
RowLayout { SnapLayout {
id: root id: root
spacing: Tokens.spacing.largeIncreased spacing: Tokens.spacing.largeIncreased
@@ -20,7 +22,7 @@ RowLayout {
readonly property real markerX: { readonly property real markerX: {
const d = root.now; const d = root.now;
return d.getHours() + d.getMinutes() / 60; return ZUtils.snap(d.getHours() + d.getMinutes() / 60, root);
} }
function formatHour(h) { function formatHour(h) {
@@ -39,11 +41,13 @@ RowLayout {
onTriggered: root.now = new Date() onTriggered: root.now = new Date()
} }
ColumnLayout { SnapLayout {
orientation: Qt.Vertical
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true
Layout.preferredWidth: root.width / 2 Layout.preferredWidth: root.width / 2
RowLayout { SnapLayout {
Layout.fillWidth: true Layout.fillWidth: true
MaterialIcon { MaterialIcon {
@@ -67,11 +71,11 @@ RowLayout {
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)
@@ -90,11 +94,13 @@ RowLayout {
} }
} }
ColumnLayout { SnapLayout {
orientation: Qt.Vertical
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true
Layout.preferredWidth: root.width / 2 Layout.preferredWidth: root.width / 2
RowLayout { SnapLayout {
Layout.fillWidth: true Layout.fillWidth: true
MaterialIcon { MaterialIcon {
@@ -126,7 +132,7 @@ RowLayout {
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 + "%"
+14 -7
View File
@@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Config import ZShell.Config
import ZShell.Components
import qs.Components import qs.Components
import qs.Modules.Dashboard.Weather import qs.Modules.Dashboard.Weather
import qs.Services import qs.Services
@@ -18,13 +19,14 @@ Item {
implicitHeight: layout.implicitHeight implicitHeight: layout.implicitHeight
Component.onCompleted: Weather.reload() Component.onCompleted: Weather.reload()
ColumnLayout { SnapLayout {
orientation: Qt.Vertical
id: layout id: layout
anchors.fill: parent anchors.fill: parent
spacing: Tokens.spacing.medium spacing: Tokens.spacing.medium
RowLayout { SnapLayout {
Layout.leftMargin: Tokens.padding.large Layout.leftMargin: Tokens.padding.large
Layout.rightMargin: Tokens.padding.large Layout.rightMargin: Tokens.padding.large
Layout.fillWidth: true Layout.fillWidth: true
@@ -82,7 +84,7 @@ Item {
} }
} }
RowLayout { SnapLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: Tokens.spacing.medium spacing: Tokens.spacing.medium
@@ -121,7 +123,9 @@ Item {
color: Colors.palette.m3onSurface color: Colors.palette.m3onSurface
} }
RowLayout { SnapLayout {
id: forecastLayout
Layout.fillWidth: true Layout.fillWidth: true
spacing: Tokens.spacing.medium spacing: Tokens.spacing.medium
@@ -137,12 +141,14 @@ Item {
required property var modelData required property var modelData
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true
implicitHeight: forecastItemColumn.implicitHeight + Tokens.padding.medium * 2 implicitHeight: forecastItemColumn.implicitHeight + Tokens.padding.medium * 2
radius: root.radius radius: root.radius
color: Colors.tPalette.m3surfaceContainer color: Colors.tPalette.m3surfaceContainer
ColumnLayout { SnapLayout {
orientation: Qt.Vertical
id: forecastItemColumn id: forecastItemColumn
anchors.centerIn: parent anchors.centerIn: parent
@@ -196,7 +202,8 @@ Item {
property color colour property color colour
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 60 Layout.fillHeight: true
implicitHeight: 60
radius: Tokens.rounding.medium radius: Tokens.rounding.medium
color: Colors.tPalette.m3surfaceContainer color: Colors.tPalette.m3surfaceContainer
@@ -230,7 +237,7 @@ Item {
} }
} }
component WeatherStat: RowLayout { component WeatherStat: SnapLayout {
id: weatherStat id: weatherStat
property string icon property string icon
@@ -1,5 +1,6 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Components
import Quickshell import Quickshell
import Quickshell.Widgets import Quickshell.Widgets
import Quickshell.Hyprland import Quickshell.Hyprland
@@ -43,7 +44,8 @@ Item {
} }
opacity: root.visible ? 1 : 0 opacity: root.visible ? 1 : 0
ColumnLayout { SnapLayout {
orientation: Qt.Vertical
id: menuLayout id: menuLayout
anchors.centerIn: parent anchors.centerIn: parent
spacing: 0 spacing: 0
@@ -53,7 +55,7 @@ Item {
radius: popupBackground.radius - popupBackground.padding radius: popupBackground.radius - popupBackground.padding
implicitHeight: openTerminalRow.implicitHeight + Tokens.padding.extraSmall * 2 implicitHeight: openTerminalRow.implicitHeight + Tokens.padding.extraSmall * 2
RowLayout { SnapLayout {
id: openTerminalRow id: openTerminalRow
spacing: 8 spacing: 8
anchors.fill: parent anchors.fill: parent
@@ -92,7 +94,7 @@ Item {
radius: popupBackground.radius - popupBackground.padding radius: popupBackground.radius - popupBackground.padding
implicitHeight: settingsRow.implicitHeight + Tokens.padding.extraSmall * 2 implicitHeight: settingsRow.implicitHeight + Tokens.padding.extraSmall * 2
RowLayout { SnapLayout {
id: settingsRow id: settingsRow
spacing: 8 spacing: 8
anchors.fill: parent anchors.fill: parent
@@ -132,7 +134,7 @@ Item {
radius: popupBackground.radius - popupBackground.padding radius: popupBackground.radius - popupBackground.padding
implicitHeight: logoutRow.implicitHeight + Tokens.padding.extraSmall * 2 implicitHeight: logoutRow.implicitHeight + Tokens.padding.extraSmall * 2
RowLayout { SnapLayout {
id: logoutRow id: logoutRow
spacing: 8 spacing: 8
anchors.fill: parent anchors.fill: parent
@@ -171,7 +173,7 @@ Item {
// radius: popupBackground.radius - popupBackground.padding // radius: popupBackground.radius - popupBackground.padding
// implicitHeight: desktopIconsRow.implicitHeight + Tokens.padding.extraSmall * 2 // implicitHeight: desktopIconsRow.implicitHeight + Tokens.padding.extraSmall * 2
// //
// RowLayout { // SnapLayout {
// id: desktopIconsRow // id: desktopIconsRow
// spacing: 8 // spacing: 8
// anchors.fill: parent // anchors.fill: parent
@@ -3,6 +3,7 @@ import QtQuick.Layouts
import Quickshell import Quickshell
import Quickshell.Widgets import Quickshell.Widgets
import qs.Components import qs.Components
import ZShell.Components
import ZShell.Config import ZShell.Config
import qs.Services import qs.Services
@@ -57,22 +58,25 @@ Item {
Anim {} Anim {}
} }
ColumnLayout { SnapLayout {
id: menuLayout id: menuLayout
anchors.centerIn: parent anchors.centerIn: parent
spacing: 0 spacing: 0
orientation: Qt.Vertical
CustomRect { CustomRect {
Layout.preferredWidth: 160 Layout.preferredWidth: 160
implicitHeight: openRow.implicitHeight + Tokens.padding.extraSmall * 2 implicitHeight: openRow.implicitHeight + Tokens.padding.extraSmall * 2
radius: popupBackground.radius - popupBackground.padding radius: popupBackground.radius - popupBackground.padding
RowLayout { SnapLayout {
id: openRow id: openRow
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: Tokens.padding.small anchors.leftMargin: Tokens.padding.small
spacing: 8 spacing: 8
MaterialIcon { MaterialIcon {
@@ -111,11 +115,12 @@ Item {
implicitHeight: openWithRow.implicitHeight + Tokens.padding.extraSmall * 2 implicitHeight: openWithRow.implicitHeight + Tokens.padding.extraSmall * 2
radius: popupBackground.radius - popupBackground.padding radius: popupBackground.radius - popupBackground.padding
RowLayout { SnapLayout {
id: openWithRow id: openWithRow
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: Tokens.padding.small anchors.leftMargin: Tokens.padding.small
spacing: 8 spacing: 8
MaterialIcon { MaterialIcon {
@@ -156,11 +161,12 @@ Item {
implicitHeight: copyPathRow.implicitHeight + Tokens.padding.extraSmall * 2 implicitHeight: copyPathRow.implicitHeight + Tokens.padding.extraSmall * 2
radius: popupBackground.radius - popupBackground.padding radius: popupBackground.radius - popupBackground.padding
RowLayout { SnapLayout {
id: copyPathRow id: copyPathRow
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: Tokens.padding.small anchors.leftMargin: Tokens.padding.small
spacing: 8 spacing: 8
MaterialIcon { MaterialIcon {
@@ -190,11 +196,12 @@ Item {
radius: popupBackground.radius - popupBackground.padding radius: popupBackground.radius - popupBackground.padding
visible: contextMenu.targetPaths.length === 1 visible: contextMenu.targetPaths.length === 1
RowLayout { SnapLayout {
id: renameRow id: renameRow
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: Tokens.padding.small anchors.leftMargin: Tokens.padding.small
spacing: 8 spacing: 8
MaterialIcon { MaterialIcon {
@@ -231,11 +238,12 @@ Item {
implicitHeight: deleteRow.implicitHeight + Tokens.padding.extraSmall * 2 implicitHeight: deleteRow.implicitHeight + Tokens.padding.extraSmall * 2
radius: popupBackground.radius - popupBackground.padding radius: popupBackground.radius - popupBackground.padding
RowLayout { SnapLayout {
id: deleteRow id: deleteRow
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: Tokens.padding.small anchors.leftMargin: Tokens.padding.small
spacing: 8 spacing: 8
MaterialIcon { MaterialIcon {
+7 -2
View File
@@ -4,6 +4,7 @@ import Quickshell
import Quickshell.Widgets import Quickshell.Widgets
import qs.Components import qs.Components
import qs.Helpers import qs.Helpers
import ZShell.Components
import ZShell.Config import ZShell.Config
import qs.Services import qs.Services
@@ -29,7 +30,10 @@ CustomRect {
active: !isSeparator active: !isSeparator
anchors.centerIn: parent anchors.centerIn: parent
sourceComponent: ColumnLayout { sourceComponent: SnapLayout {
spacing: Tokens.spacing.extraSmall
orientation: Qt.Vertical
IconImage { IconImage {
id: icon id: icon
@@ -38,8 +42,9 @@ CustomRect {
source: Quickshell.iconPath(AppSearch.guessIcon(appToplevel?.appId), "image-missing") source: Quickshell.iconPath(AppSearch.guessIcon(appToplevel?.appId), "image-missing")
} }
RowLayout { SnapLayout {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
spacing: 3 spacing: 3
Repeater { Repeater {
+1
View File
@@ -1,5 +1,6 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Components
import qs.Components import qs.Components
import ZShell.Config import ZShell.Config
import qs.Services import qs.Services
+3 -1
View File
@@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import Quickshell import Quickshell
import ZShell
import ZShell.Config import ZShell.Config
import qs.Components import qs.Components
import qs.Services import qs.Services
@@ -52,7 +53,7 @@ CustomListView {
highlightFollowsCurrentItem: false highlightFollowsCurrentItem: false
highlightRangeMode: ListView.ApplyRange highlightRangeMode: ListView.ApplyRange
implicitHeight: (Config.launcher.sizes.itemHeight + spacing) * Math.min(Config.launcher.maxAppsShown, count) - spacing implicitHeight: (Tokens.sizes.launcher.itemHeight + spacing) * Math.min(Config.launcher.maxAppsShown, count) - spacing
orientation: Qt.Vertical orientation: Qt.Vertical
preferredHighlightBegin: 0 preferredHighlightBegin: 0
preferredHighlightEnd: height preferredHighlightEnd: height
@@ -97,6 +98,7 @@ CustomListView {
} }
} }
highlight: CustomRect { highlight: CustomRect {
id: hl
color: Colors.palette.m3onSurface color: Colors.palette.m3onSurface
implicitHeight: root.currentItem?.implicitHeight ?? 0 implicitHeight: root.currentItem?.implicitHeight ?? 0
implicitWidth: root.width implicitWidth: root.width
+2 -3
View File
@@ -22,7 +22,7 @@ CarouselView {
delegateProperties: ({ delegateProperties: ({
screenState: root.screenState screenState: root.screenState
}) })
focalWidth: Config.launcher.sizes.wallpaperWidth focalWidth: Tokens.sizes.launcher.wallpaperWidth
glideDuration: 250 glideDuration: 250
glideEasingType: Easing.OutCubic glideEasingType: Easing.OutCubic
implicitHeight: tileHeight + Tokens.spacing.small / 2 + Tokens.padding.large + Tokens.padding.small implicitHeight: tileHeight + Tokens.spacing.small / 2 + Tokens.padding.large + Tokens.padding.small
@@ -41,8 +41,7 @@ CarouselView {
tileHeight: focalWidth / 16 * 9 tileHeight: focalWidth / 16 * 9
delegate: Component { delegate: Component {
WallpaperTile { WallpaperTile {}
}
} }
Component.onDestruction: Wallpapers.stopPreview() Component.onDestruction: Wallpapers.stopPreview()
+3 -2
View File
@@ -2,17 +2,18 @@ pragma ComponentBehavior: Bound
import Quickshell import Quickshell
import QtQuick import QtQuick
import ZShell
import ZShell.Config
import qs.Modules.Launcher.Services import qs.Modules.Launcher.Services
import qs.Components import qs.Components
import qs.Helpers import qs.Helpers
import ZShell.Config
import qs.Services import qs.Services
Item { Item {
id: root id: root
required property real maxHeight required property real maxHeight
readonly property int padding: Tokens.padding.largeIncreased / 4 readonly property int padding: ZUtils.snap(Tokens.padding.medium / 2, root)
required property var panels required property var panels
readonly property int rounding: Tokens.rounding.largeIncreased readonly property int rounding: Tokens.rounding.largeIncreased
required property ScreenState screenState required property ScreenState screenState
+5 -4
View File
@@ -2,11 +2,12 @@ pragma ComponentBehavior: Bound
import Quickshell import Quickshell
import QtQuick import QtQuick
import ZShell
import ZShell.Config
import qs.Paths import qs.Paths
import qs.Modules import qs.Modules
import qs.Components import qs.Components
import qs.Helpers import qs.Helpers
import ZShell.Config
import qs.Services import qs.Services
Item { Item {
@@ -71,7 +72,7 @@ Item {
PropertyChanges { PropertyChanges {
appList.active: true appList.active: true
root.implicitHeight: Math.min(root.maxHeight, appList.implicitHeight > 0 ? appList.implicitHeight : empty.implicitHeight) root.implicitHeight: Math.min(root.maxHeight, appList.implicitHeight > 0 ? appList.implicitHeight : empty.implicitHeight)
root.implicitWidth: Config.launcher.sizes.itemWidth root.implicitWidth: Tokens.sizes.launcher.itemWidth
} }
AnchorChanges { AnchorChanges {
@@ -83,8 +84,8 @@ Item {
name: "wallpapers" name: "wallpapers"
PropertyChanges { PropertyChanges {
root.implicitHeight: Config.launcher.sizes.wallpaperHeight root.implicitHeight: Tokens.sizes.launcher.wallpaperHeight
root.implicitWidth: Math.max(Config.launcher.sizes.itemWidth * 1.2, wallpaperList.implicitWidth) root.implicitWidth: Math.max(Tokens.sizes.launcher.itemWidth * 1.2, wallpaperList.implicitWidth)
wallpaperList.active: true wallpaperList.active: true
} }
} }
+1 -1
View File
@@ -13,7 +13,7 @@ Item {
anchors.left: parent?.left anchors.left: parent?.left
anchors.right: parent?.right anchors.right: parent?.right
implicitHeight: Config.launcher.sizes.itemHeight implicitHeight: Tokens.sizes.launcher.itemHeight
StateLayer { StateLayer {
radius: Tokens.rounding.medium radius: Tokens.rounding.medium
+42 -18
View File
@@ -1,10 +1,13 @@
import Quickshell import Quickshell
import Quickshell.Widgets import Quickshell.Widgets
import QtQuick import QtQuick
import QtQuick.Layouts
import ZShell.Config
import ZShell.Components
import ZShell
import qs.Modules.Launcher.Services import qs.Modules.Launcher.Services
import qs.Components import qs.Components
import qs.Helpers import qs.Helpers
import ZShell.Config
import qs.Modules import qs.Modules
import qs.Services import qs.Services
@@ -16,53 +19,72 @@ Item {
anchors.left: parent?.left anchors.left: parent?.left
anchors.right: parent?.right anchors.right: parent?.right
implicitHeight: Config.launcher.sizes.itemHeight implicitHeight: ZUtils.snap(Tokens.sizes.launcher.itemHeight, root)
StateLayer { StateLayer {
radius: Tokens.rounding.medium radius: Tokens.rounding.medium
onClicked: { onClicked: {
Apps.launch(root.modelData); // Apps.launch(root.modelData);
root.screenState.launcher = false; // root.screenState.launcher = false;
ZUtils.logPlacement(icon, "icon");
ZUtils.logPlacement(name, "name");
ZUtils.logPlacement(favoriteIcon, "favoriteIcon");
ZUtils.logPlacement(comment, "comment");
ZUtils.logPlacement(text, "text");
ZUtils.logPlacement(mainLayout, "main");
} }
} }
Item { SnapLayout {
id: mainLayout
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: Tokens.padding.medium anchors.leftMargin: Tokens.padding.medium
anchors.margins: Tokens.padding.small
anchors.rightMargin: Tokens.padding.medium anchors.rightMargin: Tokens.padding.medium
spacing: Tokens.spacing.medium
IconImage { IconImage {
id: icon id: icon
anchors.verticalCenter: parent.verticalCenter implicitSize: parent.height - Tokens.padding.small * 2
implicitSize: parent.height
source: Quickshell.iconPath(root.modelData?.icon, "image-missing") source: Quickshell.iconPath(root.modelData?.icon, "image-missing")
} }
Item { SnapLayout {
anchors.left: icon.right id: text
anchors.leftMargin: Tokens.spacing.medium
anchors.verticalCenter: icon.verticalCenter Layout.fillHeight: true
implicitHeight: name.implicitHeight + comment.implicitHeight Layout.fillWidth: true
implicitWidth: parent.width - icon.width Layout.topMargin: Tokens.padding.extraSmall
Layout.bottomMargin: Tokens.padding.extraSmall
orientation: Qt.Vertical
spacing: 0
CustomText { CustomText {
id: name id: name
Layout.fillHeight: true
Layout.fillWidth: true
text: root.modelData?.name ?? "" text: root.modelData?.name ?? ""
verticalAlignment: CustomText.AlignVCenter
horizontalAlignment: CustomText.AlignLeft
} }
CustomText { CustomText {
id: comment id: comment
anchors.top: name.bottom Layout.fillHeight: true
Layout.fillWidth: true
color: Colors.palette.m3outline color: Colors.palette.m3outline
elide: Text.ElideRight elide: Text.ElideRight
font: Tokens.font.body.small font: Tokens.font.body.small
text: (root.modelData?.comment || root.modelData?.genericName || root.modelData?.name) ?? "" text: (root.modelData?.comment || root.modelData?.genericName || root.modelData?.name) ?? ""
width: root.width - icon.width - Tokens.rounding.medium - Tokens.padding.medium * 2 verticalAlignment: CustomText.AlignVCenter
horizontalAlignment: CustomText.AlignLeft
} }
} }
@@ -70,14 +92,16 @@ Item {
id: favoriteIcon id: favoriteIcon
active: root.modelData && Strings.testRegexList(Config.launcher.favoriteApps, root.modelData.id) active: root.modelData && Strings.testRegexList(Config.launcher.favoriteApps, root.modelData.id)
anchors.right: parent.right Layout.fillHeight: true
anchors.verticalCenter: parent.verticalCenter
asynchronous: true asynchronous: true
sourceComponent: MaterialIcon { sourceComponent: MaterialIcon {
color: Colors.palette.m3primary color: Colors.palette.m3primary
fill: 1 fill: 1
text: "favorite" text: "favorite"
verticalAlignment: MaterialIcon.AlignVCenter
} }
} }
} }
+2 -1
View File
@@ -2,6 +2,7 @@ import ZShell
import Quickshell import Quickshell
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Components
import qs.Components import qs.Components
import qs.Modules import qs.Modules
import qs.Helpers import qs.Helpers
@@ -31,7 +32,7 @@ Item {
} }
} }
RowLayout { SnapLayout {
anchors.left: parent.left anchors.left: parent.left
anchors.margins: Tokens.padding.small anchors.margins: Tokens.padding.small
anchors.right: parent.right anchors.right: parent.right
+9 -4
View File
@@ -2,14 +2,15 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Config
import ZShell.Components
import qs.Paths import qs.Paths
import qs.Components import qs.Components
import qs.Helpers import qs.Helpers
import ZShell.Config
import qs.Modules import qs.Modules
import qs.Services import qs.Services
ColumnLayout { SnapLayout {
id: root id: root
readonly property real centerScale: Math.min(1, (lock.screen?.height ?? 1440) / 1440) readonly property real centerScale: Math.min(1, (lock.screen?.height ?? 1440) / 1440)
@@ -19,10 +20,13 @@ ColumnLayout {
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: false Layout.fillWidth: false
Layout.preferredWidth: centerWidth Layout.preferredWidth: centerWidth
orientation: Qt.Vertical
spacing: Tokens.spacing.largeIncreased * 2 spacing: Tokens.spacing.largeIncreased * 2
RowLayout { SnapLayout {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
spacing: Tokens.spacing.small spacing: Tokens.spacing.small
CustomText { CustomText {
@@ -109,11 +113,12 @@ ColumnLayout {
} }
} }
RowLayout { SnapLayout {
id: input id: input
anchors.fill: parent anchors.fill: parent
anchors.margins: Tokens.padding.extraSmall anchors.margins: Tokens.padding.extraSmall
spacing: Tokens.spacing.medium spacing: Tokens.spacing.medium
Item { Item {
+8 -4
View File
@@ -1,8 +1,9 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Components
import ZShell.Config
import qs.Components import qs.Components
import qs.Helpers import qs.Helpers
import ZShell.Config
import qs.Services import qs.Services
Item { Item {
@@ -13,15 +14,18 @@ Item {
implicitHeight: layout.implicitHeight implicitHeight: layout.implicitHeight
implicitWidth: layout.implicitWidth implicitWidth: layout.implicitWidth
RowLayout { SnapLayout {
id: layout id: layout
anchors.fill: parent anchors.fill: parent
spacing: Tokens.spacing.largeIncreased * 2 spacing: Tokens.spacing.largeIncreased * 2
ColumnLayout { SnapLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: Tokens.spacing.medium spacing: Tokens.spacing.medium
orientation: Qt.Vertical
WeatherInfo { WeatherInfo {
id: weather id: weather
@@ -50,8 +54,8 @@ Item {
} }
NotifDock { NotifDock {
Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true
lock: root.lock lock: root.lock
} }
} }
+1
View File
@@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound
import Quickshell import Quickshell
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Components
import qs.Modules import qs.Modules
import qs.Components import qs.Components
import qs.Helpers import qs.Helpers
+3 -1
View File
@@ -48,14 +48,16 @@ Item {
} }
} }
ColumnLayout { SnapLayout {
id: layout id: layout
anchors.left: parent.left anchors.left: parent.left
anchors.margins: Tokens.padding.extraLarge anchors.margins: Tokens.padding.extraLarge
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: Tokens.spacing.extraSmall spacing: Tokens.spacing.extraSmall
orientation: Qt.Vertical
CustomText { CustomText {
Layout.fillWidth: true Layout.fillWidth: true
+65 -18
View File
@@ -4,10 +4,13 @@ import Quickshell
import Quickshell.Widgets import Quickshell.Widgets
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Components
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 ZShell.Config
import qs.Daemons import qs.Daemons
import qs.Services import qs.Services
@@ -21,9 +24,11 @@ CustomRect {
radius: Tokens.rounding.small radius: Tokens.rounding.small
topRightRadius: Tokens.rounding.largeIncreased topRightRadius: Tokens.rounding.largeIncreased
ColumnLayout { SnapLayout {
anchors.fill: parent anchors.fill: parent
anchors.margins: Tokens.padding.large anchors.margins: Tokens.padding.large
orientation: Qt.Vertical
spacing: Tokens.spacing.small spacing: Tokens.spacing.small
CustomText { CustomText {
@@ -31,7 +36,7 @@ CustomRect {
color: Colors.palette.m3outline color: Colors.palette.m3outline
elide: Text.ElideRight elide: Text.ElideRight
font: Tokens.font.mono.builders.small.weight(Font.Medium).build() 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") text: NotifServer.notClosed.length > 0 ? qsTr("%1 notification%2").arg(NotifServer.notClosed.length).arg(NotifServer.notClosed.length === 1 ? "" : "s") : qsTr("Notifications")
} }
Item { Item {
@@ -41,28 +46,26 @@ CustomRect {
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: ColumnLayout { sourceComponent: CustomText {
spacing: Tokens.spacing.largeIncreased color: Colors.palette.m3outlineVariant
font: Tokens.font.headline.builders.small.width(90).build()
CustomText { text: qsTr("No Notifications")
Layout.alignment: Qt.AlignHCenter
color: Colors.palette.m3outlineVariant
font: Tokens.font.headline.builders.small.width(90).build()
text: qsTr("No Notifications")
}
} }
} }
CustomListView { CustomListView {
id: list
anchors.fill: parent anchors.fill: parent
clip: true clip: true
orientation: Qt.Vertical
spacing: Tokens.spacing.small spacing: Tokens.spacing.small
add: Transition { add: Transition {
@@ -80,7 +83,7 @@ CustomRect {
to: 1 to: 1
} }
} }
delegate: NotifGroup {} delegate: NotifRow {}
displaced: Transition { displaced: Transition {
Anim { Anim {
properties: "opacity,scale" properties: "opacity,scale"
@@ -94,10 +97,7 @@ CustomRect {
} }
} }
model: ScriptModel { model: ScriptModel {
values: { values: [...NotifServer.groups].filter(g => g.count > 0)
const list = NotifServer.notClosed.map(n => [n.appName, null]);
return [...new Map(list).keys()];
}
} }
move: Transition { move: Transition {
Anim { Anim {
@@ -125,4 +125,51 @@ CustomRect {
} }
} }
} }
component NotifRow: Item {
id: row
required property NotificationGroup modelData
property bool expanded: false
function dismissAll(): void {
for (const n of [...modelData.activeNotifs])
n.dismiss();
}
implicitHeight: card.implicitHeight
implicitWidth: list.width
NotifSwipeArea {
id: area
implicitHeight: card.implicitHeight
implicitWidth: row.implicitWidth
expanded: row.expanded
onDismissed: row.dismissAll()
onRequestExpand: expand => row.expanded = expand
onSwiped: row.dismissAll()
Item {
anchors.top: parent.top
implicitHeight: card.implicitHeight
implicitWidth: row.implicitWidth
clip: true
NotifGroupCard {
id: card
baseLayer: 1
container: list
expanded: row.expanded
group: row.modelData
onRequestExpand: expand => row.expanded = expand
onSwiped: notif => notif.dismiss()
}
}
}
}
} }
-315
View File
@@ -1,315 +0,0 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Widgets
import Quickshell.Services.Notifications
import QtQuick
import QtQuick.Layouts
import qs.Modules
import qs.Components
import qs.Helpers
import ZShell.Config
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
}
}
RowLayout {
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)
}
}
}
}
ColumnLayout {
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)
RowLayout {
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;
}
}
RowLayout {
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: ColumnLayout {
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, " ")
}
}
}
+3 -1
View File
@@ -5,6 +5,7 @@ import QtQuick.Layouts
import M3Shapes import M3Shapes
import ZShell.Services import ZShell.Services
import ZShell.Config import ZShell.Config
import ZShell.Components
import qs.Components import qs.Components
import qs.Effects import qs.Effects
import qs.Services import qs.Services
@@ -33,11 +34,12 @@ CustomRect {
service: Storage service: Storage
} }
RowLayout { SnapLayout {
id: layout id: layout
anchors.fill: parent anchors.fill: parent
anchors.margins: Tokens.padding.large anchors.margins: Tokens.padding.large
spacing: Tokens.spacing.largeIncreased spacing: Tokens.spacing.largeIncreased
Resource { Resource {
+4 -2
View File
@@ -1,11 +1,13 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Components
import qs.Components import qs.Components
import ZShell.Config import ZShell.Config
import qs.Helpers import qs.Helpers
import qs.Services import qs.Services
ColumnLayout { SnapLayout {
orientation: Qt.Vertical
id: root id: root
required property int rootHeight required property int rootHeight
@@ -20,7 +22,7 @@ ColumnLayout {
text: Weather.description text: Weather.description
} }
RowLayout { SnapLayout {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
spacing: Tokens.spacing.small spacing: Tokens.spacing.small
+4 -2
View File
@@ -1,5 +1,6 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Components
import M3Shapes import M3Shapes
import ZShell import ZShell
import qs.Components import qs.Components
@@ -14,7 +15,7 @@ CustomRect {
implicitHeight: header.anchors.margins + header.implicitHeight + Tokens.spacing.small + layout.implicitHeight + layout.anchors.bottomMargin implicitHeight: header.anchors.margins + header.implicitHeight + Tokens.spacing.small + layout.implicitHeight + layout.anchors.bottomMargin
radius: Tokens.rounding.small radius: Tokens.rounding.small
RowLayout { SnapLayout {
id: header id: header
anchors.left: parent.left anchors.left: parent.left
@@ -55,7 +56,8 @@ CustomRect {
orientation: VerticalFadeListView.Horizontal orientation: VerticalFadeListView.Horizontal
spacing: Tokens.spacing.medium spacing: Tokens.spacing.medium
delegate: ColumnLayout { delegate: SnapLayout {
orientation: Qt.Vertical
id: hour id: hour
readonly property bool current: ListView.view.currentItem === this readonly property bool current: ListView.view.currentItem === this
+137 -86
View File
@@ -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 {}
}
} }
} }
-510
View File
@@ -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
}
}
}
}
}
}
+17 -13
View File
@@ -2,9 +2,11 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell
import ZShell.Config
import ZShell.Components
import qs.Components import qs.Components
import qs.Helpers import qs.Helpers
import ZShell.Config
import qs.Daemons import qs.Daemons
import qs.Services import qs.Services
@@ -19,14 +21,17 @@ Item {
required property ScreenState screenState required property ScreenState screenState
required property real volume required property real volume
implicitHeight: layout.implicitHeight + Tokens.padding.smaller * 2 implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
implicitWidth: layout.implicitWidth + Tokens.padding.smaller * 2 implicitWidth: layout.implicitWidth + layout.anchors.margins * 2
ColumnLayout { SnapLayout {
id: layout id: layout
anchors.centerIn: parent anchors.fill: parent
anchors.margins: Tokens.padding.extraSmall
spacing: Tokens.spacing.medium spacing: Tokens.spacing.medium
orientation: Qt.Vertical
// Speaker volume // Speaker volume
CustomMouseArea { CustomMouseArea {
@@ -37,8 +42,8 @@ Item {
Audio.decrementVolume(); Audio.decrementVolume();
} }
implicitHeight: Config.osd.sizes.sliderHeight implicitHeight: ZUtils.snap(Config.osd.sizes.sliderHeight, root)
implicitWidth: Config.osd.sizes.sliderWidth implicitWidth: ZUtils.snap(Config.osd.sizes.sliderWidth, root)
FilledSlider { FilledSlider {
anchors.fill: parent anchors.fill: parent
@@ -63,8 +68,8 @@ Item {
Audio.decrementSourceVolume(); Audio.decrementSourceVolume();
} }
implicitHeight: Config.osd.sizes.sliderHeight implicitHeight: ZUtils.snap(Config.osd.sizes.sliderHeight, root)
implicitWidth: Config.osd.sizes.sliderWidth implicitWidth: ZUtils.snap(Config.osd.sizes.sliderWidth, root)
FilledSlider { FilledSlider {
anchors.fill: parent anchors.fill: parent
@@ -93,8 +98,8 @@ Item {
monitor.setBrightness(monitor.brightness - Config.services.brightnessIncrement); monitor.setBrightness(monitor.brightness - Config.services.brightnessIncrement);
} }
implicitHeight: Config.osd.sizes.sliderHeight implicitHeight: ZUtils.snap(Config.osd.sizes.sliderHeight, root)
implicitWidth: Config.osd.sizes.sliderWidth implicitWidth: ZUtils.snap(Config.osd.sizes.sliderWidth, root)
FilledSlider { FilledSlider {
anchors.fill: parent anchors.fill: parent
@@ -131,8 +136,7 @@ Item {
} }
} }
Behavior on opacity { Behavior on opacity {
Anim { Anim {}
}
} }
} }
} }
+6 -6
View File
@@ -2,9 +2,10 @@ pragma ComponentBehavior: Bound
import Quickshell import Quickshell
import QtQuick import QtQuick
import ZShell
import ZShell.Config
import qs.Components import qs.Components
import qs.Helpers import qs.Helpers
import ZShell.Config
import qs.Daemons import qs.Daemons
Item { Item {
@@ -17,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
@@ -30,8 +31,8 @@ Item {
} }
anchors.rightMargin: (-implicitWidth - 5 - sidebarOffset) * offsetScale anchors.rightMargin: (-implicitWidth - 5 - sidebarOffset) * offsetScale
implicitHeight: content.implicitHeight implicitHeight: ZUtils.snap(content.implicitHeight, root)
implicitWidth: content.implicitWidth implicitWidth: ZUtils.snap(content.implicitWidth, root)
opacity: 1 - offsetScale opacity: 1 - offsetScale
visible: offsetScale < 1 visible: offsetScale < 1
@@ -98,8 +99,7 @@ Item {
id: content id: content
active: root.shouldBeActive || root.visible active: root.shouldBeActive || root.visible
anchors.left: parent.left anchors.fill: parent
anchors.verticalCenter: parent.verticalCenter
sourceComponent: Content { sourceComponent: Content {
brightness: root.brightness brightness: root.brightness
+14 -5
View File
@@ -83,13 +83,16 @@ Scope {
opacity: 0 opacity: 0
radius: Tokens.rounding.small * 2 radius: Tokens.rounding.small * 2
ColumnLayout { SnapLayout {
id: layout id: layout
anchors.fill: parent anchors.fill: parent
anchors.margins: Tokens.padding.medium anchors.margins: Tokens.padding.medium
RowLayout { spacing: Tokens.spacing.extraSmall
orientation: Qt.Vertical
SnapLayout {
id: contentRow id: contentRow
spacing: 24 spacing: 24
@@ -120,12 +123,15 @@ Scope {
} }
} }
ColumnLayout { SnapLayout {
id: contentColumn id: contentColumn
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
spacing: Tokens.spacing.extraSmall
orientation: Qt.Vertical
CustomText { CustomText {
Layout.alignment: Qt.AlignLeft Layout.alignment: Qt.AlignLeft
Layout.preferredWidth: Math.min(600, contentWidth) Layout.preferredWidth: Math.min(600, contentWidth)
@@ -210,13 +216,15 @@ Scope {
} }
} }
ColumnLayout { SnapLayout {
id: textDetailsColumn id: textDetailsColumn
anchors.fill: parent anchors.fill: parent
anchors.margins: Tokens.padding.small anchors.margins: Tokens.padding.small
opacity: 0 opacity: 0
spacing: Tokens.spacing.small spacing: Tokens.spacing.small
orientation: Qt.Vertical
CustomText { CustomText {
text: `actionId: ${polkitAgent.flow?.actionId}` text: `actionId: ${polkitAgent.flow?.actionId}`
@@ -230,8 +238,9 @@ Scope {
} }
} }
RowLayout { SnapLayout {
Layout.preferredWidth: contentRow.implicitWidth Layout.preferredWidth: contentRow.implicitWidth
spacing: 8 spacing: 8
IconButton { IconButton {
+4 -2
View File
@@ -1,5 +1,6 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Components
import Quickshell.Services.UPower import Quickshell.Services.UPower
import ZShell.Config import ZShell.Config
import qs.Components import qs.Components
@@ -50,7 +51,8 @@ ClipRect {
} }
} }
component Contents: ColumnLayout { component Contents: SnapLayout {
orientation: Qt.Vertical
id: contents id: contents
required property color accentColor required property color accentColor
@@ -100,7 +102,7 @@ ClipRect {
} }
} }
RowLayout { SnapLayout {
Layout.alignment: Qt.AlignRight Layout.alignment: Qt.AlignRight
Layout.bottomMargin: -Tokens.padding.extraSmall Layout.bottomMargin: -Tokens.padding.extraSmall
Layout.rightMargin: -Tokens.padding.extraSmall Layout.rightMargin: -Tokens.padding.extraSmall
+23 -11
View File
@@ -1,5 +1,7 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell
import ZShell.Components
import ZShell.Config import ZShell.Config
import qs.Components import qs.Components
import qs.Services import qs.Services
@@ -15,10 +17,12 @@ CustomRect {
required property real usage required property real usage
color: Colors.tPalette.m3surfaceContainer color: Colors.tPalette.m3surfaceContainer
implicitHeight: Math.max(tempProg.implicitHeight + detailsRow.implicitHeight + Tokens.spacing.largeIncreased, usageColumn.implicitHeight + usageLabel.implicitHeight) + Tokens.padding.large * 2 implicitHeight: ZUtils.snap(Math.max(tempProg.implicitHeight + detailsRow.implicitHeight + Tokens.spacing.largeIncreased, usageColumn.implicitHeight + usageLabel.implicitHeight) + Tokens.padding.large * 2, root)
implicitWidth: 450 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
@@ -43,7 +47,7 @@ CustomRect {
anchors.margins: Tokens.padding.large anchors.margins: Tokens.padding.large
anchors.top: parent.top anchors.top: parent.top
fgColor: root.accent fgColor: root.accent
implicitSize: Math.max(icon.implicitWidth, icon.implicitHeight) + Tokens.padding.small * 2 implicitSize: ZUtils.snap(Math.max(icon.implicitWidth, icon.implicitHeight) + Tokens.padding.small * 2, root)
spacing: Tokens.spacing.extraSmall spacing: Tokens.spacing.extraSmall
strokeWidth: Tokens.padding.extraSmall strokeWidth: Tokens.padding.extraSmall
value: root.usage value: root.usage
@@ -62,12 +66,14 @@ CustomRect {
} }
} }
ColumnLayout { SnapLayout {
anchors.left: tempProg.right anchors.left: tempProg.right
anchors.margins: Tokens.spacing.largeIncreased anchors.margins: Tokens.spacing.largeIncreased
anchors.right: usageColumn.left anchors.right: usageColumn.left
anchors.verticalCenter: tempProg.verticalCenter anchors.verticalCenter: tempProg.verticalCenter
spacing: Tokens.spacing.extraSmall spacing: Tokens.spacing.extraSmall
orientation: Qt.Vertical
CustomText { CustomText {
color: root.accent color: root.accent
@@ -82,15 +88,19 @@ CustomRect {
} }
} }
ColumnLayout { SnapLayout {
id: detailsRow id: detailsRow
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.left: parent.left anchors.left: parent.left
anchors.margins: Tokens.padding.largeIncreased anchors.margins: Tokens.padding.largeIncreased
spacing: Tokens.spacing.extraSmall
RowLayout { objectName: "progress-layout"
spacing: Tokens.spacing.extraSmall
orientation: Qt.Vertical
SnapLayout {
Layout.leftMargin: -Tokens.padding.extraSmall Layout.leftMargin: -Tokens.padding.extraSmall
spacing: Tokens.spacing.extraSmall spacing: Tokens.spacing.extraSmall
@@ -114,26 +124,28 @@ CustomRect {
} }
} }
Column { SnapLayout {
id: usageColumn id: usageColumn
anchors.margins: Tokens.padding.large anchors.margins: Tokens.padding.large
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: 32 anchors.rightMargin: Tokens.padding.extraLarge
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: 0 spacing: 0
orientation: Qt.Vertical
CustomText { CustomText {
id: usageLabel id: usageLabel
anchors.right: parent.right Layout.alignment: Qt.AlignRight
color: Colors.palette.m3onSurfaceVariant color: Colors.palette.m3onSurfaceVariant
font: Tokens.font.body.medium font: Tokens.font.body.medium
text: qsTr("Usage") text: qsTr("Usage")
} }
CustomText { CustomText {
anchors.right: parent.right Layout.alignment: Qt.AlignRight
color: root.accent color: root.accent
font: Tokens.font.headline.builders.small.width(50).build() font: Tokens.font.headline.builders.small.width(50).build()
text: isNaN(root.usage) ? "...%" : Math.round(root.usage * 100) + "%" text: isNaN(root.usage) ? "...%" : Math.round(root.usage * 100) + "%"
+24 -17
View File
@@ -1,5 +1,7 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell
import ZShell.Components
import ZShell.Services import ZShell.Services
import ZShell.Config import ZShell.Config
import qs.Components import qs.Components
@@ -10,34 +12,39 @@ CustomRect {
readonly property color accent: Colors.palette.m3tertiary readonly property color accent: Colors.palette.m3tertiary
required property real perc
required property int used
required property int total
required property string label
required property string icon
Layout.fillWidth: true Layout.fillWidth: true
color: Colors.tPalette.m3surfaceContainer color: Colors.tPalette.m3surfaceContainer
implicitHeight: layout.implicitHeight + Tokens.padding.large * 2 implicitHeight: layout.implicitHeight + Tokens.padding.large * 2
implicitWidth: layout.implicitWidth + Tokens.padding.extraLargeIncreased * 2 implicitWidth: layout.implicitWidth + Tokens.padding.extraLargeIncreased * 2
radius: Tokens.rounding.largeIncreased radius: Tokens.rounding.largeIncreased
ServiceRef { SnapLayout {
service: Memory
}
ColumnLayout {
id: layout id: layout
anchors.centerIn: parent anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
orientation: Qt.Vertical
spacing: Tokens.spacing.extraSmall spacing: Tokens.spacing.extraSmall
RowLayout { SnapLayout {
Layout.leftMargin: -Tokens.padding.extraSmall Layout.alignment: Qt.AlignHCenter
spacing: Tokens.spacing.small spacing: Tokens.spacing.small
MaterialIcon { MaterialIcon {
color: root.accent color: root.accent
fill: 1 fill: 1
text: "memory_alt" text: root.icon
} }
CustomText { CustomText {
text: qsTr("Memory") text: root.label
} }
} }
@@ -45,18 +52,18 @@ CustomRect {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Layout.topMargin: Tokens.spacing.largeIncreased Layout.topMargin: Tokens.spacing.largeIncreased
fgColor: root.accent fgColor: root.accent
implicitSize: usageColumn.implicitHeight + thickness + Tokens.padding.largeIncreased * 2 implicitSize: ZUtils.snap(usageColumn.implicitHeight + thickness + Tokens.padding.largeIncreased * 2, root)
startAngle: -225 startAngle: -225
sweepAngle: 270 sweepAngle: 270
value: Memory.percentage value: root.perc
Behavior on clampedVal { Behavior on clampedVal {
Anim { Anim {}
}
} }
ColumnLayout { SnapLayout {
id: usageColumn id: usageColumn
orientation: Qt.Vertical
anchors.centerIn: parent anchors.centerIn: parent
anchors.verticalCenterOffset: Tokens.padding.extraSmall anchors.verticalCenterOffset: Tokens.padding.extraSmall
@@ -66,7 +73,7 @@ CustomRect {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
color: root.accent color: root.accent
font: Tokens.font.body.large font: Tokens.font.body.large
text: Math.round(Memory.percentage * 100) + "%" text: Math.round(root.perc * 100) + "%"
} }
CustomText { CustomText {
@@ -80,7 +87,7 @@ CustomRect {
CustomText { CustomText {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
text: { text: {
const fmt = UsageFmt.formatKib(Memory.used, Memory.total); const fmt = UsageFmt.formatKib(root.used, root.total);
return `${fmt.value.toFixed(1)} / ${Math.floor(fmt.total)} ${fmt.unit}`; return `${fmt.value.toFixed(1)} / ${Math.floor(fmt.total)} ${fmt.unit}`;
} }
} }

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