Merge branch 'main' into module/wifi
C++ / fmt (pull_request) Successful in 7s
C++ / build (pull_request) Failing after 12s
C++ / clang-tidy (pull_request) Failing after 11s
JS/TS / fmt (pull_request) Successful in 13s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Successful in 35s
Rust / build (pull_request) Successful in 1m0s
Python / verify (pull_request) Successful in 1m44s
Rust / fmt (pull_request) Successful in 45s
Rust / clippy (pull_request) Successful in 1m7s

This commit is contained in:
2026-10-22 23:21:20 +02:00
469 changed files with 40652 additions and 7947 deletions
+21 -42
View File
@@ -80,49 +80,29 @@ Slider {
property bool moving
function update(): void {
animate = !moving;
binding.when = moving;
font.pointSize = moving ? Tokens.font.size.small : Tokens.font.size.larger;
font.family = moving ? Config.appearance.font.family.sans : Config.appearance.font.family.material;
}
anchors.centerIn: parent
anchors.verticalCenterOffset: 1
text: moving ? Math.round(root.value * 100) : root.icon
color: Colors.palette.m3inverseOnSurface
text: root.icon
font: moving ? Tokens.font.body.small : Tokens.font.icon.medium
onMovingChanged: anim.restart()
Binding {
id: binding
property: "text"
target: icon
value: Math.round(root.value * root.multiplier)
when: false
}
SequentialAnimation {
id: anim
Anim {
duration: Tokens.anim.durations.normal / 2
easing: Tokens.anim.standardAccel
property: "scale"
target: icon
to: 0
}
ScriptAction {
script: icon.update()
}
Anim {
duration: Tokens.anim.durations.normal / 2
easing: Tokens.anim.standardDecel
property: "scale"
target: icon
to: 1
Behavior on moving {
SequentialAnimation {
Anim {
target: icon
property: "scale"
to: 0.3
duration: Tokens.anim.durations.small / 2
easing: Tokens.anim.standardAccel
}
PropertyAction {}
Anim {
target: icon
property: "scale"
to: 1
duration: Tokens.anim.durations.normal / 2
easing: Tokens.anim.standardDecel
}
}
}
}
@@ -130,7 +110,7 @@ Slider {
}
Behavior on value {
Anim {
duration: Tokens.anim.durations.large
type: Anim.StandardLarge
}
}
@@ -138,7 +118,6 @@ Slider {
onValueChanged: {
if (!initialized) {
initialized = true;
oldValue = value;
return;
}
+2 -6
View File
@@ -20,11 +20,7 @@ CustomRect {
property color disabledColor: Qt.alpha(Colors.palette.m3onSurface, 0.1)
property color disabledOnColor: Qt.alpha(Colors.palette.m3onSurface, 0.38)
property bool fillWidth
property font font: ({
family: Config.appearance.font.family.sans,
pointSize: Tokens.font.size.larger,
bold: false
})
property font font: Tokens.font.body.small
property real horizontalPadding: padding
readonly property alias hovered: stateLayer.containsMouse
required implicitHeight
@@ -55,7 +51,7 @@ CustomRect {
if (internalChecked)
return checkedRadius;
if (isRound)
return (height || implicitHeight) / 2 * Math.min(1, Tokens.rounding.scale);
return implicitHeight / 2 * Math.min(1, Tokens.rounding.scale);
return defaultRadius;
}
+1 -1
View File
@@ -20,7 +20,7 @@ BusyIndicator {
property int animState
property color bgColor: Colors.palette.m3secondaryContainer
property color fgColor: Colors.palette.m3primary
property real implicitSize: Tokens.font.size.normal * 3
property real implicitSize: Tokens.font.body.medium.pointSize * 3
property real internalStrokeWidth: strokeWidth
readonly property alias progress: manager.progress
property real strokeWidth: Tokens.padding.extraSmall * 0.8
+5 -5
View File
@@ -46,10 +46,10 @@ Item {
}
function syncFromPenColor() {
if (!drawing.drawingState)
if (!drawing.screenState)
return;
const c = drawing.drawingState.penColor;
const c = drawing.screenState.penColor;
if (c.hsvSaturation > 0) {
currentHue = c.hsvHue;
@@ -86,7 +86,7 @@ Item {
currentHue = relative / arcSweep;
lastChromaticHue = currentHue;
drawing.drawingState.penColor = Qt.hsva(currentHue, drawing.drawingState.penColor.hsvSaturation, drawing.drawingState.penColor.hsvValue, drawing.drawingState.penColor.a);
drawing.screenState.penColor = Qt.hsva(currentHue, drawing.screenState.penColor.hsvSaturation, drawing.screenState.penColor.hsvValue, drawing.screenState.penColor.a);
}
implicitHeight: 180
@@ -112,7 +112,7 @@ Item {
root.syncFromPenColor();
}
target: root.drawing.drawingState
target: root.drawing.screenState
}
Canvas {
@@ -152,7 +152,7 @@ Item {
CustomRect {
anchors.centerIn: parent
color: root.drawing?.drawingState.penColor
color: root.drawing?.screenState.penColor
implicitHeight: implicitWidth
implicitWidth: canvas.height - root.handleSize - Tokens.padding.extraLarge * 2
radius: Tokens.rounding.full
+6 -3
View File
@@ -1,12 +1,12 @@
import QtQuick
import qs.Helpers
import qs.Services
Flickable {
id: root
property bool doneFakeFlick
interactive: !Visibilities.getForActive().isDrawing
interactive: !ShellState.forActive().isDrawing
maximumFlickVelocity: 3000
rebound: Transition {
@@ -28,6 +28,9 @@ Flickable {
interval: 10
running: root.doneFakeFlick
onTriggered: root.doneFakeFlick = false
onTriggered: {
root.doneFakeFlick = false;
root.returnToBounds();
}
}
}
+19 -16
View File
@@ -1,33 +1,36 @@
import QtQuick
import qs.Helpers
import qs.Services
ListView {
id: root
property bool doneFakeFlick
interactive: !Visibilities.getForActive()?.isDrawing
interactive: !ShellState.forActive()?.isDrawing
maximumFlickVelocity: 3000
rebound: Transition {
onRunningChanged: {
if (!running && !root.doneFakeFlick) {
root.doneFakeFlick = true;
root.flick(1, 1);
root.flick(-1, -1);
Qt.callLater(() => root.cancelFlick());
}
}
// onRunningChanged: {
// if (!running && !root.doneFakeFlick) {
// root.doneFakeFlick = true;
// root.flick(1, 1);
// root.flick(-1, -1);
// Qt.callLater(() => root.cancelFlick());
// }
// }
Anim {
properties: "x,y"
type: Anim.DefaultEffects
}
}
Timer {
interval: 10
running: root.doneFakeFlick
onTriggered: root.doneFakeFlick = false
}
// Timer {
// interval: 10
// running: root.doneFakeFlick
//
// onTriggered: {
// root.doneFakeFlick = false;
// }
// }
}
+1
View File
@@ -4,6 +4,7 @@ MouseArea {
property int scrollAccumulatedY: 0
function onWheel(event: WheelEvent): void {
event.accepted = false;
}
onWheel: event => {
+3 -4
View File
@@ -6,7 +6,7 @@ import qs.Services
RadioButton {
id: root
font.pointSize: Tokens.font.size.normal
font: Tokens.font.body.medium
implicitHeight: Math.max(implicitIndicatorHeight, implicitContentHeight)
implicitWidth: implicitIndicatorWidth + implicitContentWidth + contentItem.anchors.leftMargin
@@ -14,7 +14,7 @@ RadioButton {
anchors.left: outerCircle.right
anchors.leftMargin: 10
anchors.verticalCenter: parent.verticalCenter
font.pointSize: root.font.pointSize
font: root.font
text: root.text
}
indicator: Rectangle {
@@ -29,8 +29,7 @@ RadioButton {
radius: Tokens.rounding.full
Behavior on border.color {
CAnim {
}
CAnim {}
}
StateLayer {
+1 -2
View File
@@ -6,7 +6,6 @@ Rectangle {
color: "transparent"
Behavior on color {
CAnim {
}
CAnim {}
}
}
+69 -81
View File
@@ -1,32 +1,41 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Templates
import qs.Helpers
import ZShell.Config
import qs.Services
import ZShell.Config
ScrollBar {
id: root
required property Flickable flickable
readonly property bool isHorizontal: flickable && flickable.ScrollBar.horizontal === root
readonly property real axisSize: isHorizontal ? flickable.width : flickable.height
readonly property real axisContentSize: isHorizontal ? flickable.contentWidth : flickable.contentHeight
readonly property real axisContentPos: isHorizontal ? (reversed ? flickable.contentX : (flickable.contentX - flickable.originX)) : (reversed ? flickable.contentY : (flickable.contentY - flickable.originY))
readonly property real axisLength: isHorizontal ? root.width : root.height
readonly property real effectiveSize: Math.max(nonAnimHeight, root.minimumSize)
readonly property real effectiveTravel: Math.max(0, 1 - root.effectiveSize)
required property Flickable flickable
readonly property real nonAnimHeight: flickable.height / flickable.contentHeight
readonly property real nonAnimY: flickable.contentY / flickable.contentHeight
readonly property real nonAnimHeight: root.axisSize / root.axisContentSize
readonly property real nonAnimY: root.axisContentPos / root.axisContentSize
readonly property real rawTravel: Math.max(0, 1 - root.nonAnimHeight)
readonly property bool reversed: flickable instanceof ListView && flickable.verticalLayoutDirection === ListView.BottomToTop
property bool shouldBeActive
readonly property bool reversed: isHorizontal ? (flickable instanceof ListView && flickable.layoutDirection === Qt.RightToLeft) : (flickable instanceof ListView && flickable.verticalLayoutDirection === ListView.BottomToTop)
readonly property real travelScale: root.rawTravel > 0 ? root.effectiveTravel / root.rawTravel : 0
enabled: !Visibilities.getForActive().isDrawing
implicitWidth: Tokens.padding.extraSmall * 2
property bool shouldBeActive
property bool inverted
contentItem: Item {
}
enabled: !ShellState.forActive().isDrawing
implicitWidth: size === 1 ? 0 : isHorizontal ? 0 : Tokens.padding.extraSmall * 2
implicitHeight: size === 1 ? 0 : isHorizontal ? Tokens.padding.extraSmall * 2 : 0
contentItem: Item {}
Behavior on position {
enabled: !fullMouse.pressed
Anim {
}
Anim {}
}
onHoveredChanged: {
@@ -47,54 +56,27 @@ ScrollBar {
target: root.flickable
}
CustomClippingRect {
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.top: parent.top
implicitWidth: handle.implicitWidth
radius: Tokens.rounding.full
Loader {
anchors.fill: parent
active: root.size < 1
sourceComponent: root.isHorizontal ? horizontalTrack : verticalTrack
}
CustomRect {
id: handle
Component {
id: verticalTrack
anchors.right: parent.right
color: Colors.palette.m3secondary
implicitHeight: root.height * root.effectiveSize
implicitWidth: fullMouse.pressed || fullMouse.containsMouse ? Tokens.padding.extraSmall * 2 : Tokens.padding.extraSmall
opacity: {
if (!root.enabled)
return 0;
if (root.size === 1)
return 0;
if (fullMouse.pressed)
return 1;
if (fullMouse.containsMouse)
return 0.8;
if (root.policy === ScrollBar.AlwaysOn || root.shouldBeActive)
return 0.6;
return 0;
}
radius: Tokens.rounding.full
y: root.reversed ? root.height * (1 + root.nonAnimY) * root.travelScale : root.height * root.nonAnimY * root.travelScale
VerticalScrollBarTrack {
scrollBar: root
mouseArea: fullMouse
}
}
Behavior on implicitWidth {
Anim {
}
}
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
Component {
id: horizontalTrack
MouseArea {
id: mouse
acceptedButtons: Qt.NoButton
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
}
HorizontalScrollBarTrack {
scrollBar: root
mouseArea: fullMouse
}
}
@@ -111,25 +93,40 @@ ScrollBar {
property real pressOffset: 0
function contentYFromThumbTop(thumbTop) {
var visualPos = root.effectiveTravel > 0 ? thumbTop / root.travelScale : 0;
return root.reversed ? (visualPos - 1) * root.flickable.contentHeight : visualPos * root.flickable.contentHeight;
function contentPosFromThumbStart(thumbStart) {
var visualPos = root.effectiveTravel > 0 ? thumbStart / root.travelScale : 0;
return root.reversed ? (visualPos - 1) * root.axisContentSize : visualPos * root.axisContentSize;
}
function updateFromEvent(event) {
var posInTrack = event.y / root.height;
var thumbTop = posInTrack - pressOffset;
thumbTop = Math.max(0, Math.min(root.effectiveTravel, thumbTop));
var eventPos = root.isHorizontal ? event.x : event.y;
var posInTrack = eventPos / root.axisLength;
var thumbStart = posInTrack - pressOffset;
thumbStart = Math.max(0, Math.min(root.effectiveTravel, thumbStart));
root.flickable.contentY = contentYFromThumbTop(thumbTop);
var newPos = contentPosFromThumbStart(thumbStart);
if (root.isHorizontal)
root.flickable.contentX = newPos + (root.flickable.originX + (root.reversed ? root.flickable.contentWidth : 0));
else
root.flickable.contentY = newPos + (root.flickable.originY + (root.reversed ? root.flickable.contentHeight : 0));
}
function visualThumbTop() {
function visualThumbStart() {
const visualPos = root.reversed ? (1 + root.nonAnimY) : root.nonAnimY;
return visualPos * root.travelScale;
}
function onWheel(event: WheelEvent): void {
if (root.horizontal) {
event.accepted = false;
return;
}
var delta = event.angleDelta.y > 0 ? -0.1 : 0.1;
var newPos = Math.max(0, Math.min(1 - root.size, root.position + (root.inverted ? -delta : delta)));
root.position = newPos;
}
anchors.fill: parent
cursorShape: undefined
hoverEnabled: true
@@ -140,24 +137,15 @@ ScrollBar {
updateFromEvent(event);
}
onPressed: event => {
var currentTop = visualThumbTop();
var currentBottom = currentTop + root.effectiveSize;
var clickPos = event.y / root.height;
var currentStart = visualThumbStart();
var currentEnd = currentStart + root.effectiveSize;
var eventPos = root.isHorizontal ? event.x : event.y;
var clickPos = eventPos / root.axisLength;
var clickedInsideThumb = clickPos >= currentTop && clickPos <= currentBottom;
if (clickedInsideThumb) {
pressOffset = clickPos - currentTop;
} else {
pressOffset = root.effectiveSize / 2;
}
var clickedInsideThumb = clickPos >= currentStart && clickPos <= currentEnd;
pressOffset = clickedInsideThumb ? (clickPos - currentStart) : root.effectiveSize / 2;
updateFromEvent(event);
}
onWheel: event => {
var delta = event.angleDelta.y > 0 ? -0.1 : 0.1;
var newPos = Math.max(0, Math.min(1 - root.size, root.position + delta));
root.position = newPos;
}
}
}
+9 -7
View File
@@ -100,15 +100,19 @@ Slider {
anchors.bottom: parent.bottom
anchors.top: parent.top
color: root.insetColor
font.pixelSize: Tokens.font.size.smaller * 2
fontStyle: {
if (root.height < 96)
return Tokens.font.icon.pixelSize(24).build();
else
return Tokens.font.icon.pixelSize(32).build();
}
text: root.insetIcon
verticalAlignment: Text.AlignVCenter
visible: !root.wavy && root.insetIcon !== ""
x: Tokens.spacing.extraSmall + dockT * (handle.x + Tokens.spacing.small)
Behavior on dockT {
Anim {
}
Anim {}
}
}
@@ -138,8 +142,7 @@ Slider {
startX: x
Behavior on color {
CAnim {
}
CAnim {}
}
Anim on waveProgress {
duration: root.waveDuration
@@ -156,8 +159,7 @@ Slider {
Behavior on filledWidth {
id: widthBehavior
Anim {
}
Anim {}
}
Component.onCompleted: {
+2 -3
View File
@@ -14,10 +14,9 @@ Text {
property real animateTo: 1
color: Colors.palette.m3onSurface
font.family: Config.appearance.font.family.sans
font.pointSize: Tokens.font.size.normal
font: Tokens.font.body.small
linkColor: Colors.palette.m3onPrimaryFixedVariant
renderType: Text.NativeRendering
renderType: Text.QtRendering
textFormat: Text.PlainText
Behavior on color {
+26 -23
View File
@@ -15,29 +15,33 @@ TextFieldBase {
readonly property int clampedRadius: Math.min(horizontalPadding, Math.min(width, height) / 2, radius)
readonly property string effectiveSupportingText: isError && errorText ? errorText : supportingText
readonly property int filledOffset: type === CustomTextField.Filled ? Tokens.spacing.small : 0
readonly property int leadingOffset: leadingIcon ? leadingIconLoader.width + leadingIconLoader.anchors.leftMargin : 0
readonly property real smallFontScale: smallFontSize / font.pointSize
readonly property int supportingTextOffset: effectiveSupportingText ? supportingTextLoader.height + Tokens.spacing.extraSmall : 0
readonly property int trailingOffset: trailingIcon ? trailingIconLoader.width + trailingIconLoader.anchors.rightMargin : 0
readonly property bool valid: !validate || (!text && emptyIsValid) || (validate instanceof RegExp ? validate.test(text) : !!validate(text))
property int horizontalPadding: Tokens.padding.large
property int verticalPadding: Tokens.padding.large
property int radius: Tokens.rounding.small
property string supportingText
property string trailingIcon
property int smallFontSize: Tokens.font.label.small.pointSize
property int type: CustomTextField.Outlined
property var validate // Regex or function
property bool emptyIsValid: true
property string errorText
readonly property int filledOffset: type === CustomTextField.Filled ? Tokens.spacing.small : 0
readonly property int horizontalPadding: Tokens.padding.large
property bool isError
property string leadingIcon
readonly property int leadingOffset: leadingIcon ? leadingIconLoader.width + leadingIconLoader.anchors.leftMargin : 0
property int radius: Tokens.rounding.small
property alias sLayer: stateLayer
readonly property real smallFontScale: smallFontSize / font.pointSize
property int smallFontSize: Tokens.font.size.small
property string supportingText
readonly property int supportingTextOffset: effectiveSupportingText ? supportingTextLoader.height + Tokens.spacing.extraSmall : 0
property string trailingIcon
readonly property int trailingOffset: trailingIcon ? trailingIconLoader.width + trailingIconLoader.anchors.rightMargin : 0
property int type: CustomTextField.Outlined
readonly property bool valid: !validate || (!text && emptyIsValid) || (validate instanceof RegExp ? validate.test(text) : !!validate(text))
property var validate // Regex or function
bottomPadding: Tokens.padding.large + supportingTextOffset - filledOffset
bottomPadding: verticalPadding + supportingTextOffset - filledOffset
leftPadding: horizontalPadding + leadingOffset
rightPadding: horizontalPadding + trailingOffset
topPadding: Tokens.padding.large + filledOffset
topPadding: verticalPadding + filledOffset
background: Loader {
anchors.bottomMargin: root.supportingTextOffset
@@ -148,6 +152,7 @@ TextFieldBase {
color: Colors.palette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.larger
text: root.leadingIcon
fontStyle: Tokens.font.icon.builders.medium.scale(0.9).build()
}
}
@@ -162,6 +167,7 @@ TextFieldBase {
sourceComponent: MaterialIcon {
color: Colors.palette.m3onSurfaceVariant
text: root.trailingIcon
fontStyle: Tokens.font.icon.builders.medium.scale(0.9).build()
}
}
}
@@ -176,7 +182,7 @@ TextFieldBase {
sourceComponent: CustomText {
color: root.isError ? Colors.palette.m3error : Colors.palette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.small
font: Tokens.font.label.small
text: root.effectiveSupportingText
}
}
@@ -214,12 +220,10 @@ TextFieldBase {
}
}
Behavior on strokeColor {
CAnim {
}
CAnim {}
}
Behavior on strokeWidth {
Anim {
}
Anim {}
}
PathLine {
@@ -294,8 +298,7 @@ TextFieldBase {
implicitHeight: root.activeFocus ? 2 : 1
Behavior on implicitHeight {
Anim {
}
Anim {}
}
}
}
+42
View File
@@ -0,0 +1,42 @@
import QtQuick
import Quickshell
ShaderEffect {
id: root
property Item target: parent
property real glowSize: 40
property real innerGlow: 0
property real lineWidth: 2
property real intensity: 1.0
property real speed: 1.0
property int period: 6000
property bool running: true
property color color1: "#4285F4"
property color color2: "#9B72CB"
property color color3: "#D96570"
property color color4: "#F2A54A"
readonly property size itemSize: Qt.size(width, height)
readonly property size rectSize: target ? Qt.size(target.width, target.height) : Qt.size(0, 0)
readonly property real radius: (target as Rectangle).radius ?? 0
property real phase: 0
anchors.fill: target
anchors.margins: -glowSize
z: -1
enabled: false
fragmentShader: Qt.resolvedUrl(`${Quickshell.shellDir}/assets/shaders/geminiglow.frag.qsb`)
FrameAnimation {
running: root.running && root.visible
onTriggered: {
const dt = frameTime * 1000;
root.phase = (root.phase + dt * root.speed / root.period) % 1.0;
}
}
}
+56
View File
@@ -0,0 +1,56 @@
import QtQuick
import ZShell.Config
import qs.Services
CustomClippingRect {
id: track
required property var scrollBar
required property var mouseArea
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
implicitHeight: handle.implicitHeight
radius: Tokens.rounding.full
CustomRect {
id: handle
anchors.bottom: parent.bottom
color: Colors.palette.m3secondary
implicitWidth: track.scrollBar.width * track.scrollBar.effectiveSize
implicitHeight: track.mouseArea.pressed || track.mouseArea.containsMouse ? Tokens.padding.extraSmall * 2 : Tokens.padding.extraSmall
opacity: {
if (!track.scrollBar.enabled)
return 0;
if (track.scrollBar.size === 1)
return 0;
if (track.mouseArea.pressed)
return 1;
if (track.mouseArea.containsMouse)
return 0.8;
if (track.scrollBar.policy === CustomScrollBar.AlwaysOn || track.scrollBar.shouldBeActive)
return 0.6;
return 0;
}
radius: Tokens.rounding.full
x: track.scrollBar.reversed ? track.scrollBar.width * (1 + track.scrollBar.nonAnimY) * track.scrollBar.travelScale : track.scrollBar.width * track.scrollBar.nonAnimY * track.scrollBar.travelScale
Behavior on implicitHeight {
Anim {}
}
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
MouseArea {
acceptedButtons: Qt.NoButton
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
}
}
}
+2 -1
View File
@@ -8,6 +8,7 @@ ButtonBase {
property alias icon: label.text
readonly property alias label: label
font: Tokens.font.icon.medium
activeColor: type === IconButton.Filled ? Colors.palette.m3primary : Colors.palette.m3secondary
activeOnColor: type === IconButton.Filled ? Colors.palette.m3onPrimary : type === IconButton.Tonal ? Colors.palette.m3onSecondary : Colors.palette.m3primary
implicitHeight: {
@@ -36,7 +37,7 @@ ButtonBase {
anchors.verticalCenterOffset: 1
color: root.onColor
fill: !root.isToggle || root.internalChecked ? 1 : 0
font.pointSize: root.font.pointSize
fontStyle: root.font
Behavior on fill {
Anim {
+1 -2
View File
@@ -46,10 +46,9 @@ ButtonBase {
Layout.alignment: Qt.AlignVCenter
color: root.onColor
fill: root.internalChecked ? 1 : 0
font: {
fontStyle: {
const f = Qt.font(root.font);
f.pointSize = Math.round(root.font.pointSize * 1.2);
f.family = "Material Symbols Rounded";
return f;
}
+467
View File
@@ -0,0 +1,467 @@
pragma ComponentBehavior: Bound
import QtQuick
Item {
id: root
required property list<var> values
property int xSteps: 24
property real yMin: NaN
property real yMax: NaN
property real yPaddingTop: 0
property real yPaddingBottom: 0
property int verticalLineStep: 0
property int horizontalLineStep: 0
property bool firstXLine: true
property bool lastXLine: false
property int minHorizontalLines: 0
property var xLabel: null
property var yLabel: null
property real smoothing: 0
property bool curveSmoothing: true
property real markerX: -1
property real markerGap: 2 // px gap between line and marker dot edge, each side
property color lineColor: "white"
property color gridColor: "white"
property color markerColor: "white"
property color labelColor: "white"
property real gridAlpha: 0.12
property real labelAlpha: 0.6
property real fillAlpha: 0.15
property real lineWidth: 2
property real markerDotRadius: 4
property real labelFontSize: 10
Canvas {
id: canvas
anchors.fill: parent
renderStrategy: Canvas.Immediate
onWidthChanged: requestPaint()
onHeightChanged: requestPaint()
function strokeCurve(ctx, pts) {
ctx.beginPath();
ctx.moveTo(pts[0].x, pts[0].y);
if (root.curveSmoothing && pts.length > 2) {
const k = 1 / 6;
const n = pts.length;
for (let i = 0; i < n - 1; i++) {
const p0 = pts[Math.max(i - 1, 0)];
const p1 = pts[i];
const p2 = pts[i + 1];
const p3 = pts[Math.min(i + 2, n - 1)];
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);
}
}
}
// Control points for segment i (pts[i] -> pts[i+1]), matching strokeCurve
function segmentControls(pts, i) {
const p0 = pts[Math.max(i - 1, 0)];
const p1 = pts[i];
const p2 = pts[i + 1];
const p3 = pts[Math.min(i + 2, pts.length - 1)];
if (root.curveSmoothing && pts.length > 2) {
const k = 1 / 6;
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));
return [c1x, c1y, c2x, c2y];
}
const cx = (p1.x + p2.x) * 0.5;
return [cx, p1.y, cx, p2.y];
}
// Point on the curve at a given x (x is monotonic per segment)
function pointAtX(pts, targetX) {
let j = pts.length - 2;
for (let i = 0; i < pts.length - 1; i++) {
if (targetX <= pts[i + 1].x) {
j = i;
break;
}
}
const p1 = pts[j];
const p2 = pts[j + 1];
const [c1x, c1y, c2x, c2y] = segmentControls(pts, j);
let tlo = 0;
let thi = 1;
for (let it = 0; it < 24; 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;
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 };
}
// Part of the curve left/right of a cut x, including the cut point
function cutLeft(pts, targetX) {
if (targetX <= pts[0].x)
return [];
if (targetX >= pts[pts.length - 1].x)
return pts.slice();
let j = pts.length - 2;
for (let i = 0; i < pts.length - 1; i++) {
if (targetX <= pts[i + 1].x) {
j = i;
break;
}
}
const left = pts.slice(0, j + 1);
if (targetX > pts[j].x)
left.push(pointAtX(pts, targetX));
return left;
}
function cutRight(pts, targetX) {
if (targetX >= pts[pts.length - 1].x)
return [];
if (targetX <= pts[0].x)
return pts.slice();
let j = pts.length - 2;
for (let i = 0; i < pts.length - 1; i++) {
if (targetX <= pts[i + 1].x) {
j = i;
break;
}
}
const right = pts.slice(j + 1);
if (targetX < pts[j + 1].x)
right.unshift(pointAtX(pts, targetX));
return right;
}
onPaint: {
const ctx = getContext("2d");
ctx.reset();
const W = width;
const H = height;
if (W < 2 || H < 2)
return;
const padLeft = root.labelFontSize * 3.4;
const padRight = 2;
const padTop = 2;
const padBottom = root.labelFontSize + 8;
const plot = {
x: padLeft,
y: padTop,
w: W - padLeft - padRight,
h: H - padTop - padBottom
};
if (plot.w < 2 || plot.h < 2)
return;
// Auto-fit: NaN yMin/yMax derive from the data, snapped to the
// horizontal step grid, honoring minHorizontalLines
let lo = root.yMin;
let hi = root.yMax;
const loAuto = Number.isNaN(lo);
const hiAuto = Number.isNaN(hi);
if (loAuto || hiAuto) {
let dmin = Infinity;
let dmax = -Infinity;
for (const v of root.values) {
if (v == null)
continue;
if (v < dmin)
dmin = v;
if (v > dmax)
dmax = v;
}
if (dmin === Infinity) {
dmin = 0;
dmax = 1;
}
const step = root.horizontalLineStep;
const minSteps = root.minHorizontalLines > 1 ? root.minHorizontalLines - 1 : 0;
if (step > 0) {
if (hiAuto) {
const baseLo = loAuto ? Math.floor(dmin / step) * step : lo;
const span = Math.max(Math.ceil((dmax - baseLo) / step), minSteps, 1);
lo = baseLo;
hi = baseLo + span * step;
} else {
const span = Math.max(Math.ceil((hi - dmin) / step), minSteps, 1);
lo = hi - span * step;
}
} else {
if (loAuto)
lo = dmin;
if (hiAuto)
hi = dmax;
if (hi <= lo)
hi = lo + 1;
}
}
const scaleBottom = lo - root.yPaddingBottom;
const scaleTop = hi + root.yPaddingTop;
const range = scaleTop - scaleBottom;
if (range <= 0 || root.xSteps < 1)
return;
let series = root.values;
if (root.smoothing > 0 && series.length > 0) {
const radius = Math.round(root.smoothing);
const raw = series;
series = raw.map((v, i) => {
if (v == null)
return null;
let sum = 0;
let n = 0;
for (let j = -radius; j <= radius; j++) {
const u = raw[i + j];
if (u != null) {
sum += u;
n++;
}
}
return n > 0 ? sum / n : null;
});
}
const xAt = i => plot.x + (i / root.xSteps) * plot.w;
const yAt = v => plot.y + (1 - (v - scaleBottom) / range) * plot.h;
const crisp = v => Math.floor(v) + 0.5;
// 1. Grid lines
ctx.strokeStyle = Qt.alpha(root.gridColor, root.gridAlpha);
ctx.lineWidth = 1;
if (root.verticalLineStep > 0) {
for (let i = 0; i <= root.xSteps; i += root.verticalLineStep) {
if (i === 0 && !root.firstXLine)
continue;
if (i === root.xSteps && !root.lastXLine)
continue;
const x = crisp(xAt(i));
ctx.beginPath();
ctx.moveTo(x, plot.y);
ctx.lineTo(x, H);
ctx.stroke();
}
}
// Lines extend down into yPaddingBottom, snapped to the grid
const vStart = lo - (root.yPaddingBottom > 0 ? Math.floor(root.yPaddingBottom / root.horizontalLineStep) * root.horizontalLineStep : 0);
if (root.horizontalLineStep > 0) {
for (let v = vStart; v <= hi + 0.001; v += root.horizontalLineStep) {
const y = crisp(yAt(v));
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(plot.x + plot.w, y);
ctx.stroke();
}
}
// 2. Labels
ctx.fillStyle = Qt.alpha(root.labelColor, root.labelAlpha);
ctx.font = root.labelFontSize + "px sans-serif";
ctx.textAlign = "left";
ctx.textBaseline = "top";
if (root.verticalLineStep > 0 && root.xLabel) {
for (let i = 0; i <= root.xSteps; i += root.verticalLineStep) {
if (i === 0 && !root.firstXLine)
continue;
if (i === root.xSteps && !root.lastXLine)
continue;
const text = root.xLabel(i);
if (text)
ctx.fillText(text, xAt(i) + 4, plot.y + plot.h + 4);
}
}
ctx.textBaseline = "bottom";
if (root.horizontalLineStep > 0 && root.yLabel) {
for (let v = vStart; v <= hi + 0.001; v += root.horizontalLineStep) {
const text = root.yLabel(v);
if (text)
ctx.fillText(text, 2, yAt(v) - 2);
}
}
// 3. Curve, split into segments at null gaps
const segments = [];
let current = null;
for (let i = 0; i < series.length; i++) {
if (series[i] == null) {
if (current)
segments.push(current);
current = null;
continue;
}
const pt = {
x: xAt(i),
y: yAt(series[i])
};
if (!current)
current = [];
current.push(pt);
}
if (current)
segments.push(current);
ctx.lineCap = "round";
ctx.lineJoin = "round";
const hasMarker = root.markerX >= 0 && root.markerX <= root.xSteps;
const markerPx = hasMarker ? xAt(root.markerX) : 0;
let gapPx = 0;
if (hasMarker) {
// markerGap is measured from the dot edge, perpendicular to the
// curve; project onto x via the local curve slope
let slope = 0;
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)
slope = (pointAtX(seg, markerPx + 1).y - pointAtX(seg, markerPx - 1).y) / 2;
gapPx = (root.markerDotRadius + root.markerGap) / Math.sqrt(1 + slope * slope);
}
for (const seg of segments) {
if (seg.length < 2)
continue;
// Fill stays continuous under the marker
ctx.fillStyle = Qt.alpha(root.lineColor, root.fillAlpha);
strokeCurve(ctx, seg);
ctx.lineTo(seg[seg.length - 1].x, plot.y + plot.h);
ctx.lineTo(seg[0].x, plot.y + plot.h);
ctx.closePath();
ctx.fill();
// Stroke has a gap around the marker
const pieces = hasMarker ? [cutLeft(seg, markerPx - gapPx), cutRight(seg, markerPx + gapPx)] : [seg];
for (const pts of pieces) {
if (pts.length < 2)
continue;
ctx.strokeStyle = root.lineColor;
ctx.lineWidth = root.lineWidth;
strokeCurve(ctx, pts);
ctx.stroke();
}
}
// 4. Marker: dashed vertical + dot
if (root.markerX >= 0 && root.markerX <= root.xSteps) {
const x = markerPx;
ctx.setLineDash([4, 4]);
ctx.strokeStyle = Qt.alpha(root.markerColor, 0.6);
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(crisp(x), plot.y);
ctx.lineTo(crisp(x), H);
ctx.stroke();
ctx.setLineDash([]);
const i0 = Math.floor(root.markerX);
const v0 = series[i0];
const v1 = series[i0 + 1];
let y = null;
if (v0 != null && v1 != null)
y = yAt(v0 + (v1 - v0) * (root.markerX - i0));
else if (v0 != null)
y = yAt(v0);
if (y != null) {
ctx.fillStyle = root.markerColor;
ctx.beginPath();
ctx.arc(x, y, root.markerDotRadius, 0, Math.PI * 2);
ctx.fill();
}
}
}
}
onValuesChanged: canvas.requestPaint()
onXStepsChanged: canvas.requestPaint()
onYMinChanged: canvas.requestPaint()
onYMaxChanged: canvas.requestPaint()
onYPaddingTopChanged: canvas.requestPaint()
onYPaddingBottomChanged: canvas.requestPaint()
onVerticalLineStepChanged: canvas.requestPaint()
onHorizontalLineStepChanged: canvas.requestPaint()
onFirstXLineChanged: canvas.requestPaint()
onLastXLineChanged: canvas.requestPaint()
onMinHorizontalLinesChanged: canvas.requestPaint()
onXLabelChanged: canvas.requestPaint()
onYLabelChanged: canvas.requestPaint()
onSmoothingChanged: canvas.requestPaint()
onCurveSmoothingChanged: canvas.requestPaint()
onMarkerXChanged: canvas.requestPaint()
onMarkerGapChanged: canvas.requestPaint()
onLineColorChanged: canvas.requestPaint()
onGridColorChanged: canvas.requestPaint()
onMarkerColorChanged: canvas.requestPaint()
onLabelColorChanged: canvas.requestPaint()
onGridAlphaChanged: canvas.requestPaint()
onLabelAlphaChanged: canvas.requestPaint()
onFillAlphaChanged: canvas.requestPaint()
onLineWidthChanged: canvas.requestPaint()
onMarkerDotRadiusChanged: canvas.requestPaint()
onLabelFontSizeChanged: canvas.requestPaint()
}
+4 -17
View File
@@ -1,27 +1,14 @@
import QtQuick
import ZShell.Config
import qs.Services
CustomText {
id: root
readonly property list<int> allowed: [20, 24, 40, 48]
property real fill: 0
property int grade: Colors.light ? 0 : -25
readonly property int opsz: clampOpsz(optical)
property int optical: fontInfo.pixelSize
property font fontStyle: Tokens.font.icon.small
function clampOpsz(value): int {
return allowed.reduce((closest, current) => {
return Math.abs(current - value) < Math.abs(closest - value) ? current : closest;
});
}
font.family: "Material Symbols Rounded"
font.pointSize: Tokens.font.size.larger
font.variableAxes: ({
FILL: fill,
GRAD: grade,
opsz: opsz,
wght: fontInfo.weight
})
renderType: Text.NativeRendering
font: Tokens.font.icon.size(fontStyle.pointSize).weight(fontStyle.weight).vaxes(fontStyle.variableAxes).fill(fill.toFixed(1)).grade(grade).build()
}
+13 -12
View File
@@ -124,7 +124,7 @@ MouseArea {
model: root.items
CustomRect {
id: item
id: menuItem
readonly property bool active: modelData === root?.active
required property int index
@@ -142,13 +142,13 @@ MouseArea {
}
StateLayer {
color: item.active ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurface
color: menuItem.active ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurface
enabled: root.expanded
onClicked: {
root.itemSelected(item.modelData);
root.active = item.modelData;
item.modelData.clicked();
root.itemSelected(menuItem.modelData);
root.active = menuItem.modelData;
menuItem.modelData.clicked();
root.expanded = false;
}
}
@@ -162,26 +162,27 @@ MouseArea {
MaterialIcon {
Layout.alignment: Qt.AlignVCenter
color: item.active ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurfaceVariant
text: item.modelData?.icon ?? ""
color: menuItem.active ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurfaceVariant
text: menuItem.modelData?.icon ?? ""
visible: text !== ""
}
CustomText {
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
color: item.active ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurface
text: item.modelData?.text ?? ""
color: menuItem.active ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurface
text: menuItem.modelData?.text ?? ""
}
Loader {
Layout.alignment: Qt.AlignVCenter
active: item.modelData?.trailingIcon.length > 0
active: menuItem.modelData?.trailingIcon.length > 0
asynchronous: true
visible: active
sourceComponent: MaterialIcon {
color: item.active ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurfaceVariant
text: item.modelData.trailingIcon
color: menuItem.active ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurfaceVariant
text: menuItem.modelData.trailingIcon
}
}
}
+40
View File
@@ -0,0 +1,40 @@
import QtQuick
import Quickshell
PersistentProperties {
required property ShellScreen modelData
reloadableId: "screenState"
// Drawer visibilities
property bool bar
property bool clipboard
property bool dashboard
property bool dock
property bool isDrawing
property bool launcher
property bool osd
property bool resources
property bool settings
property bool sidebar
// Dashboard state
property int dashboardTab: 0
property date dashboardDate: new Date()
// Sidebar state
property int sidebarTab: 0
property list<string> expandedNotifs: []
// Utilities state
property string recordingConfirmDelete
property bool recordingListExpanded
property string recordingMode
// Drawing state
property color penColor: "white"
property int penWidth: 4
// Screenshot state
property bool screenshot
}
+1 -1
View File
@@ -62,7 +62,7 @@ TextFieldBase {
anchors.leftMargin: Tokens.padding.large
anchors.verticalCenter: parent.verticalCenter
color: Colors.palette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.large
fontStyle: Tokens.font.icon.builders.medium.scale(0.9).build()
text: "search"
}
+3 -4
View File
@@ -2,7 +2,6 @@ import QtQuick
import QtQuick.Shapes
import ZShell
import ZShell.Config
import qs.Helpers
import qs.Services
MouseArea {
@@ -52,7 +51,7 @@ MouseArea {
anchors.fill: parent
cursorShape: !enabled ? undefined : Qt.PointingHandCursor
enabled: parent.enabled && !Visibilities.getForActive()?.isDrawing
enabled: parent.enabled && !ShellState.forActive()?.isDrawing
hoverEnabled: true
Behavior on stateOpacity {
@@ -99,13 +98,13 @@ MouseArea {
id: base
anchors.fill: parent
bottomLeftRadius: root.parent?.bottomLeftRadius ?? radius ?? 0
bottomRightRadius: root.parent?.bottomRightRadius ?? radius ?? 0
color: Colors.palette.m3onSurface
opacity: root.stateOpacity
// Pick up radius from parent if it has one (parent can be anything with radius props)
// qmllint disable missing-property
radius: root.parent?.radius ?? 0
bottomLeftRadius: root.parent?.bottomLeftRadius ?? radius ?? 0
bottomRightRadius: root.parent?.bottomRightRadius ?? radius ?? 0
topLeftRadius: root.parent?.topLeftRadius ?? radius ?? 0
topRightRadius: root.parent?.topRightRadius ?? radius ?? 0
// qmllint enable missing-property
+86
View File
@@ -0,0 +1,86 @@
import QtQuick
import QtQuick.Templates
import ZShell.Config
import qs.Services
TextArea {
id: root
color: Colors.palette.m3onSurface
cursorVisible: !readOnly
font: Tokens.font.body.small
implicitHeight: contentHeight + topPadding + bottomPadding
implicitWidth: contentWidth + leftPadding + rightPadding
placeholderTextColor: Colors.palette.m3onSurfaceVariant // No anim cause placeholder is custom
renderType: TextArea.NativeRendering
selectedTextColor: color
selectionColor: Qt.alpha(Colors.palette.m3primary, 0.4)
verticalAlignment: TextInput.AlignVCenter
Behavior on color {
CAnim {}
}
Behavior on selectionColor {
CAnim {}
}
cursorDelegate: Item {}
CustomRect {
id: cursor
property bool disableBlink
color: Colors.palette.m3primary
implicitHeight: root.cursorRectangle.height
implicitWidth: 1.5
radius: Tokens.rounding.largeIncreased
x: root.cursorRectangle.x
y: root.cursorRectangle.y
Behavior on x {
Anim {
duration: Tokens.anim.durations.expressiveFastEffects
easing.bezierCurve: [0.2, 1, 0.21, 1, 1, 1]
}
}
Behavior on opacity {
Anim {
type: Anim.StandardSmall
}
}
Connections {
function onCursorPositionChanged(): void {
if (root.activeFocus && root.cursorVisible) {
cursor.opacity = 1;
cursor.disableBlink = true;
enableBlink.restart();
}
}
target: root
}
Timer {
id: enableBlink
interval: 500
onTriggered: cursor.disableBlink = false
}
Timer {
interval: 500
repeat: true
running: root.activeFocus && root.cursorVisible && !cursor.disableBlink
triggeredOnStart: true
onTriggered: parent.opacity = parent.opacity === 1 ? 0 : 1
}
Binding {
cursor.opacity: 0
when: !root.activeFocus || !root.cursorVisible
}
}
}
+96
View File
@@ -0,0 +1,96 @@
import QtQuick
import QtQuick.Templates
import ZShell.Config
import qs.Services
TextEdit {
id: root
property bool animateCursor: true
property alias cursor: cursor
color: Colors.palette.m3onSurface
cursorVisible: !readOnly
font: Tokens.font.body.small
renderType: TextField.NativeRendering
selectedTextColor: color
selectionColor: Qt.alpha(Colors.palette.m3primary, 0.4)
verticalAlignment: TextInput.AlignVCenter
Behavior on color {
CAnim {}
}
cursorDelegate: Item {}
Behavior on selectionColor {
CAnim {}
}
CustomRect {
id: cursor
property bool disableBlink
color: Colors.palette.m3primary
implicitHeight: root.cursorRectangle.height
implicitWidth: 1.5
radius: Tokens.rounding.largeIncreased
x: root.cursorRectangle.x
y: root.cursorRectangle.y
Behavior on opacity {
Anim {
type: Anim.StandardSmall
}
}
Behavior on x {
enabled: root.animateCursor
Anim {
duration: Tokens.anim.durations.expressiveFastEffects
easing.bezierCurve: [0.2, 1, 0.21, 1, 1, 1]
}
}
Behavior on y {
enabled: root.animateCursor
Anim {
duration: Tokens.anim.durations.expressiveFastEffects
easing.bezierCurve: [0.2, 1, 0.21, 1, 1, 1]
}
}
Connections {
function onCursorPositionChanged(): void {
if (root.activeFocus && root.cursorVisible) {
cursor.opacity = 1;
cursor.disableBlink = true;
enableBlink.restart();
}
}
target: root
}
Timer {
id: enableBlink
interval: 500
onTriggered: cursor.disableBlink = false
}
Timer {
interval: 500
repeat: true
running: root.activeFocus && root.cursorVisible && !cursor.disableBlink
triggeredOnStart: true
onTriggered: parent.opacity = parent.opacity === 1 ? 0 : 1
}
Binding {
cursor.opacity: 0
when: !root.activeFocus || !root.cursorVisible
}
}
}
+4 -7
View File
@@ -8,7 +8,7 @@ TextField {
color: Colors.palette.m3onSurface
cursorVisible: !readOnly
font.pointSize: Tokens.font.size.small
font: Tokens.font.body.small
implicitHeight: contentHeight + topPadding + bottomPadding
implicitWidth: contentWidth + leftPadding + rightPadding
placeholderTextColor: Colors.palette.m3onSurfaceVariant // No anim cause placeholder is custom
@@ -18,14 +18,11 @@ TextField {
verticalAlignment: TextInput.AlignVCenter
Behavior on color {
CAnim {
}
}
cursorDelegate: Item {
CAnim {}
}
cursorDelegate: Item {}
Behavior on selectionColor {
CAnim {
}
CAnim {}
}
CustomRect {
+3 -4
View File
@@ -36,8 +36,7 @@ CustomRect {
radius: Tokens.rounding.medium
Behavior on border.color {
CAnim {
}
CAnim {}
}
Elevation {
@@ -84,7 +83,7 @@ CustomRect {
return Colors.palette.m3onError;
return Colors.palette.m3onSurfaceVariant;
}
font.pointSize: Math.round(Tokens.font.size.large * 1.2)
fontStyle: Tokens.font.icon.builders.large.scale(1.2).build()
text: root.modelData.icon
}
}
@@ -107,7 +106,7 @@ CustomRect {
return Colors.palette.m3onSurface;
}
elide: Text.ElideRight
font.pointSize: Tokens.font.size.normal
font: Tokens.font.title.small
text: root.modelData.title
}
+7 -3
View File
@@ -7,7 +7,9 @@ CustomListView {
id: root
property real endFadeOpacity: fadeShouldBeActive(false) ? 0 : 1
property bool fadeEnabled: true
property real fadeAmount: 0.1
property real fadeThreshold: 0.0
readonly property bool horizontal: orientation === ListView.Horizontal
property real startFadeOpacity: fadeShouldBeActive(true) ? 0 : 1
@@ -16,7 +18,9 @@ CustomListView {
}
function fadeShouldBeActive(isStart: bool): bool {
// When content is smaller than flickable size, hide fade when rebound starts.
if (!fadeEnabled)
return false;
if (contentSize() + marginStart() + marginEnd() < viewportSize() && rebound.running && ((isStart ? overshootStart() > 0 : overshootStart() < 0))) {
return false;
}
@@ -28,11 +32,11 @@ CustomListView {
}
function marginEnd(): real {
return horizontal ? rightMargin : bottomMargin;
return horizontal ? rightMargin - fadeThreshold : bottomMargin - fadeThreshold;
}
function marginStart(): real {
return horizontal ? leftMargin : topMargin;
return horizontal ? leftMargin - fadeThreshold : topMargin - fadeThreshold;
}
function overshootStart(): real {
+56
View File
@@ -0,0 +1,56 @@
import QtQuick
import ZShell.Config
import qs.Services
CustomClippingRect {
id: track
required property var scrollBar
required property var mouseArea
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.top: parent.top
implicitWidth: handle.implicitWidth
radius: Tokens.rounding.full
CustomRect {
id: handle
anchors.right: parent.right
color: Colors.palette.m3secondary
implicitHeight: track.scrollBar.height * track.scrollBar.effectiveSize
implicitWidth: track.mouseArea.pressed || track.mouseArea.containsMouse ? Tokens.padding.extraSmall * 2 : Tokens.padding.extraSmall
opacity: {
if (!track.scrollBar.enabled)
return 0;
if (track.scrollBar.size === 1)
return 0;
if (track.mouseArea.pressed)
return 1;
if (track.mouseArea.containsMouse)
return 0.8;
if (track.scrollBar.policy === CustomScrollBar.AlwaysOn || track.scrollBar.shouldBeActive)
return 0.6;
return 0;
}
radius: Tokens.rounding.full
y: track.scrollBar.reversed ? track.scrollBar.height * (1 + track.scrollBar.nonAnimY) * track.scrollBar.travelScale : track.scrollBar.height * track.scrollBar.nonAnimY * track.scrollBar.travelScale
Behavior on implicitWidth {
Anim {}
}
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
MouseArea {
acceptedButtons: Qt.NoButton
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
}
}
}