updated components #122

Merged
zach merged 64 commits from feat/updated-components into main 2026-06-23 11:33:20 +02:00
201 changed files with 11432 additions and 4202 deletions
+51
View File
@@ -0,0 +1,51 @@
import QtQuick
import qs.Config
AnchorAnimation {
enum Type {
StandardSmall = 0,
Standard,
StandardLarge,
StandardExtraLarge,
EmphasizedSmall,
Emphasized,
EmphasizedLarge,
EmphasizedExtraLarge,
FastSpatial,
DefaultSpatial,
SlowSpatial
}
property int type: AnchorAnim.DefaultSpatial
duration: {
if (type < AnchorAnim.StandardSmall || type > AnchorAnim.SlowSpatial)
return Appearance.anim.durations.expressiveDefaultSpatial;
if (type == AnchorAnim.FastSpatial)
return Appearance.anim.durations.expressiveFastSpatial;
if (type == AnchorAnim.DefaultSpatial)
return Appearance.anim.durations.expressiveDefaultSpatial;
if (type == AnchorAnim.SlowSpatial)
return Appearance.anim.durations.large;
const types = ["small", "normal", "large", "extraLarge"];
const idx = type % 4;
return Appearance.anim.durations[types[idx]];
}
easing.bezierCurve: {
if (type == AnchorAnim.FastSpatial)
return Appearance.anim.curves.expressiveFastSpatial;
if (type == AnchorAnim.DefaultSpatial)
return Appearance.anim.curves.expressiveDefaultSpatial;
if (type == AnchorAnim.SlowSpatial)
return Appearance.anim.curves.expressiveSlowSpatial;
if (type >= AnchorAnim.StandardSmall && type <= AnchorAnim.StandardExtraLarge)
return Appearance.anim.curves.standard;
if (type >= AnchorAnim.EmphasizedSmall && type <= AnchorAnim.EmphasizedExtraLarge)
return Appearance.anim.curves.emphasized;
return Appearance.anim.curves.expressiveDefaultSpatial;
}
}
+58 -3
View File
@@ -2,7 +2,62 @@ import QtQuick
import qs.Config
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.bezierCurve: Appearance.anim.curves.standard
easing.type: Easing.BezierSpline
enum Type {
StandardSmall = 0,
Standard,
StandardLarge,
StandardExtraLarge,
EmphasizedSmall,
Emphasized,
EmphasizedLarge,
EmphasizedExtraLarge,
FastSpatial,
DefaultSpatial,
SlowSpatial,
FastEffects,
DefaultEffects,
SlowEffects
}
property int type: Anim.DefaultSpatial
duration: {
if (type < Anim.StandardSmall || type > Anim.SlowEffects)
return Appearance.anim.durations.normal;
if (type === Anim.FastSpatial)
return Appearance.anim.durations.expressiveFastSpatial;
if (type === Anim.DefaultSpatial)
return Appearance.anim.durations.expressiveDefaultSpatial;
if (type === Anim.SlowSpatial)
return Appearance.anim.durations.large;
if (type === Anim.FastEffects)
return Appearance.anim.durations.expressiveFastEffects;
if (type === Anim.DefaultEffects)
return Appearance.anim.durations.expressiveEffects;
if (type === Anim.SlowEffects)
return Appearance.anim.durations.expressiveSlowEffects;
const types = ["small", "normal", "large", "extraLarge"];
const idx = type % 4;
return Appearance.anim.durations[types[idx]];
}
easing.bezierCurve: {
if (type === Anim.FastSpatial)
return Appearance.anim.curves.expressiveFastSpatial;
if (type === Anim.DefaultSpatial)
return Appearance.anim.curves.expressiveDefaultSpatial;
if (type === Anim.SlowSpatial)
return Appearance.anim.curves.expressiveSlowSpatial;
if (type === Anim.FastEffects)
return Appearance.anim.curves.expressiveFastEffects;
if (type === Anim.DefaultEffects)
return Appearance.anim.curves.expressiveDefaultEffects;
if (type === Anim.SlowEffects)
return Appearance.anim.curves.expressiveSlowEffects;
if (type >= Anim.EmphasizedSmall && type <= Anim.EmphasizedExtraLarge)
return Appearance.anim.curves.emphasized;
return Appearance.anim.curves.standard;
}
}
-3
View File
@@ -12,11 +12,8 @@ Slider {
readonly property bool isVertical: orientation === Qt.Vertical
property real multiplier: 100
property real oldValue
// Wrapper components can inject their own track visuals here.
property Component trackContent
// Keep current behavior for existing usages.
orientation: Qt.Vertical
background: CustomRect {
+87
View File
@@ -0,0 +1,87 @@
import QtQuick
import qs.Config
CustomRect {
id: root
enum ButtonType {
Filled,
Tonal,
Text
}
property color activeColor
property color activeOnColor
property bool checked
property real checkedRadius: Appearance.rounding.medium
property real defaultRadius: Appearance.rounding.large
property color disabledColor: Qt.alpha(DynamicColors.palette.m3onSurface, 0.1)
property color disabledOnColor: Qt.alpha(DynamicColors.palette.m3onSurface, 0.38)
property bool fillWidth
property font font: ({
family: Appearance.font.family.sans,
pointSize: Appearance.font.size.larger,
bold: false
})
property real horizontalPadding: padding
readonly property alias hovered: stateLayer.containsMouse
required implicitHeight
required implicitWidth
property color inactiveColor
property color inactiveOnColor
property bool internalChecked
property bool isRound
property bool isToggle
readonly property color onColor: !enabled ? disabledOnColor : internalChecked ? activeOnColor : inactiveOnColor
property real padding
readonly property alias pressed: stateLayer.pressed
property real pressedRadius: Appearance.rounding.small
readonly property alias radiusAnim: radiusAnim
property bool radiusMorph: true
property alias shapeMorph: stateLayer.shapeMorph
property real shapeMorphExpansion: shapeMorph && pressed ? 24 : 0
readonly property alias stateLayer: stateLayer
property int type: ButtonBase.Filled
property real verticalPadding: padding
signal clicked
color: type === ButtonBase.Text ? "transparent" : !enabled ? disabledColor : internalChecked ? activeColor : inactiveColor
radius: {
if (radiusMorph && pressed)
return pressedRadius;
if (internalChecked)
return checkedRadius;
if (isRound)
return (height || implicitHeight) / 2 * Math.min(1, Appearance.rounding.scale);
return defaultRadius;
}
Behavior on radius {
Anim {
id: radiusAnim
type: Anim.DefaultEffects
}
}
Behavior on shapeMorphExpansion {
Anim {
type: Anim.FastSpatial
}
}
onCheckedChanged: internalChecked = checked
StateLayer {
id: stateLayer
color: root.internalChecked ? root.activeOnColor : root.inactiveOnColor
enabled: root.enabled
onClicked: {
if (root.isToggle)
root.internalChecked = !root.internalChecked;
root.clicked();
}
}
}
+4 -5
View File
@@ -17,8 +17,8 @@ BusyIndicator {
}
property int animState
property color bgColour: DynamicColors.palette.m3secondaryContainer
property color fgColour: DynamicColors.palette.m3primary
property color bgColor: DynamicColors.palette.m3secondaryContainer
property color fgColor: DynamicColors.palette.m3primary
property real implicitSize: Appearance.font.size.normal * 3
property real internalStrokeWidth: strokeWidth
readonly property alias progress: manager.progress
@@ -31,8 +31,8 @@ BusyIndicator {
contentItem: CircularProgress {
anchors.fill: parent
bgColour: root.bgColour
fgColour: root.fgColour
bgColor: root.bgColor
fgColor: root.fgColor
padding: root.padding
rotation: manager.rotation
startAngle: manager.startFraction * 360
@@ -73,7 +73,6 @@ BusyIndicator {
CircularIndicatorManager {
id: manager
}
NumberAnimation {
+89 -39
View File
@@ -1,66 +1,116 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Shapes
import ZShell.Components
import qs.Config
Shape {
Item {
id: root
readonly property real arcRadius: (size - padding - strokeWidth) / 2
property color bgColour: DynamicColors.palette.m3secondaryContainer
property color fgColour: DynamicColors.palette.m3primary
readonly property real arcRadius: (size - padding - strokeWidth * (1 + waveAmplitude * 2)) / 2
property color bgColor: DynamicColors.palette.m3secondaryContainer
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
property color fgColor: DynamicColors.palette.m3primary
readonly property real gapAngle: ((spacing + strokeWidth) / (arcRadius || 1)) * (180 / Math.PI)
property alias hasEndIndicator: dot.active
property real implicitSize
property int padding: 0
readonly property real size: Math.min(width, height)
property int spacing: Appearance.spacing.small
property int startAngle: -90
property int strokeWidth: Appearance.padding.smaller
readonly property real vValue: value || 1 / 360
property int strokeWidth: Appearance.padding.small
property int sweepAngle: 360
readonly property real thickness: strokeWidth * (1 + waveAmplitude) * 2
property real value
property alias waveAmplitude: wave.amplitudeMultiplier
property alias waveDuration: waveProgAnim.duration
property alias waveFrequency: wave.frequency
property bool wavePaused
property bool wavy: false
asynchronous: true
preferredRendererType: Shape.CurveRenderer
implicitHeight: implicitSize
implicitWidth: implicitSize
ShapePath {
capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap
fillColor: "transparent"
strokeColor: root.bgColour
strokeWidth: root.strokeWidth
Shape {
asynchronous: true
opacity: Math.min(1, remainingArc.sweepAngle)
preferredRendererType: Shape.CurveRenderer
Behavior on strokeColor {
CAnim {
duration: Appearance.anim.durations.large
ShapePath {
capStyle: ShapePath.RoundCap
fillColor: "transparent"
strokeColor: root.bgColor
strokeWidth: Math.min(1, remainingArc.sweepAngle) * root.strokeWidth
Behavior on strokeColor {
CAnim {
}
}
}
PathAngleArc {
centerX: root.size / 2
centerY: root.size / 2
radiusX: root.arcRadius
radiusY: root.arcRadius
startAngle: root.startAngle + 360 * root.vValue + root.gapAngle
sweepAngle: Math.max(-root.gapAngle, 360 * (1 - root.vValue) - root.gapAngle * 2)
PathAngleArc {
id: remainingArc
centerX: root.size / 2
centerY: root.size / 2
radiusX: root.arcRadius
radiusY: root.arcRadius
startAngle: root.startAngle + root.clampedVal * root.sweepAngle + root.gapAngle
sweepAngle: Math.max(1 / 360, root.sweepAngle * (1 - root.clampedVal) - root.gapAngle * (root.sweepAngle < 360 ? 1 : 2))
}
}
}
ShapePath {
capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap
fillColor: "transparent"
strokeColor: root.fgColour
strokeWidth: root.strokeWidth
WavyLine {
id: wave
Behavior on strokeColor {
CAnim {
duration: Appearance.anim.durations.large
amplitudeMultiplier: root.wavy ? 0.5 : 0
anchors.fill: parent
anchors.margins: -lineWidth * amplitudeMultiplier
color: root.fgColor
frequency: 8
fullAngle: root.sweepAngle
lineWidth: root.strokeWidth
pathType: WavyLine.Arc
radius: root.arcRadius
startAngle: root.startAngle
value: root.clampedVal
Behavior on amplitudeMultiplier {
Anim {
type: Anim.DefaultEffects
}
}
Behavior on color {
CAnim {
}
}
Anim on waveProgress {
id: waveProgAnim
PathAngleArc {
centerX: root.size / 2
centerY: root.size / 2
radiusX: root.arcRadius
radiusY: root.arcRadius
startAngle: root.startAngle
sweepAngle: 360 * root.vValue
duration: 2000
easing.type: Easing.Linear
from: 0
loops: Animation.Infinite
paused: root.wavePaused || wave.amplitudeMultiplier === 0
running: true
to: 1
}
}
Loader {
id: dot
x: root.size / 2 + root.arcRadius * Math.cos(root.dotAngleRad) - width / 2
y: root.size / 2 + root.arcRadius * Math.sin(root.dotAngleRad) - height / 2
sourceComponent: CustomRect {
color: root.fgColor
implicitHeight: Math.min(1, remainingArc.sweepAngle) * Math.min(4, root.strokeWidth)
implicitWidth: Math.min(1, remainingArc.sweepAngle) * Math.min(4, root.strokeWidth)
opacity: Math.min(1, remainingArc.sweepAngle)
radius: Appearance.rounding.full
}
}
}
+5 -5
View File
@@ -59,15 +59,15 @@ ColumnLayout {
}
StateLayer {
function onClicked(): void {
root.toggleRequested();
root.expanded = !root.expanded;
}
anchors.fill: parent
color: DynamicColors.palette.m3onSurface
radius: Appearance.rounding.normal
showHoverBackground: false
onClicked: {
root.toggleRequested();
root.expanded = !root.expanded;
}
}
}
+28 -5
View File
@@ -45,10 +45,10 @@ Item {
}
function syncFromPenColor() {
if (!drawing)
if (!drawing.drawingState)
return;
const c = drawing.penColor;
const c = drawing.drawingState.penColor;
if (c.hsvSaturation > 0) {
currentHue = c.hsvHue;
@@ -85,12 +85,20 @@ Item {
currentHue = relative / arcSweep;
lastChromaticHue = currentHue;
drawing.penColor = Qt.hsva(currentHue, drawing.penColor.hsvSaturation, drawing.penColor.hsvValue, drawing.penColor.a);
drawing.drawingState.penColor = Qt.hsva(currentHue, drawing.drawingState.penColor.hsvSaturation, drawing.drawingState.penColor.hsvValue, drawing.drawingState.penColor.a);
}
implicitHeight: 180
implicitWidth: 220
Behavior on currentHue {
enabled: !root.dragActive
Anim {
type: Anim.StandardLarge
}
}
Component.onCompleted: syncFromPenColor()
onCurrentHueChanged: canvas.requestPaint()
onDrawingChanged: syncFromPenColor()
@@ -103,7 +111,7 @@ Item {
root.syncFromPenColor();
}
target: root.drawing
target: root.drawing.drawingState
}
Canvas {
@@ -141,6 +149,21 @@ Item {
}
}
CustomRect {
anchors.centerIn: parent
color: root.drawing?.drawingState.penColor
implicitHeight: implicitWidth
implicitWidth: canvas.height - root.handleSize - Appearance.padding.extraLarge * 2
radius: Appearance.rounding.full
Behavior on color {
enabled: false
CAnim {
}
}
}
Item {
id: handle
@@ -174,7 +197,7 @@ Item {
Rectangle {
anchors.centerIn: parent
color: root.drawing ? root.drawing.penColor : Qt.hsla(root.currentHue, 1.0, 0.5, 1.0)
color: Qt.hsla(root.currentHue, 1.0, 0.5, 1.0)
height: width
radius: width / 2
width: parent.width - 12
+3 -3
View File
@@ -23,10 +23,10 @@ Button {
}
StateLayer {
function onClicked(): void {
radius: control.radius
onClicked: {
control.clicked();
}
radius: control.radius
}
}
+13
View File
@@ -0,0 +1,13 @@
import Quickshell.Widgets
import QtQuick
ClippingWrapperRectangle {
id: root
color: "transparent"
Behavior on color {
CAnim {
}
}
}
+2
View File
@@ -1,8 +1,10 @@
import QtQuick
import qs.Helpers
Flickable {
id: root
interactive: !Visibilities.getForActive().isDrawing
maximumFlickVelocity: 3000
rebound: Transition {
+20
View File
@@ -1,13 +1,33 @@
import QtQuick
import qs.Helpers
ListView {
id: root
property bool doneFakeFlick
interactive: !Visibilities.getForActive().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());
}
}
Anim {
properties: "x,y"
}
}
Timer {
interval: 10
running: root.doneFakeFlick
onTriggered: root.doneFakeFlick = false
}
}
+237
View File
@@ -0,0 +1,237 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Templates
import ZShell
import ZShell.Components
import ZShell.Internal
import qs.Config
ProgressBar {
id: root
enum IndeterminateAnimState {
Running,
Completing,
Stopped
}
property color bgColor: DynamicColors.palette.m3secondaryContainer
property color fgColor: DynamicColors.palette.m3primary
property int indeterminateAnimState: CustomProgressBar.Stopped
property real waveAmplitude: 0.5
property int waveDuration: 1000
property int waveFrequency: 6
property bool wavePaused
property bool wavy
function toBounds(startFrac: real, endFrac: real, gapSize: real): point {
startFrac = ZUtils.clamp(startFrac, 0, 1);
endFrac = ZUtils.clamp(endFrac, 0, 1);
// Ramp down gap size
const GAP_RAMP_DOWN_THRESHOLD = 0.01;
gapSize += height / 2;
const startGapSize = (gapSize * ZUtils.clamp(startFrac, 0, GAP_RAMP_DOWN_THRESHOLD) / GAP_RAMP_DOWN_THRESHOLD);
const endGapSize = (gapSize * (1 - ZUtils.clamp(endFrac, 1 - GAP_RAMP_DOWN_THRESHOLD, 1)) / GAP_RAMP_DOWN_THRESHOLD);
const start = width * startFrac + startGapSize;
const end = width * endFrac - endGapSize;
return start >= end ? Qt.point(0, 0) : Qt.point(start, end);
}
function updateIAnimState(): void {
if (indeterminate) {
manager.completeEndProgress = 0;
indeterminateAnimState = CustomProgressBar.Running;
} else if (indeterminateAnimState === CustomProgressBar.Running) {
indeterminateAnimState = CustomProgressBar.Completing;
}
}
implicitHeight: 4
implicitWidth: 200
contentItem: Loader {
anchors.fill: parent
asynchronous: true
sourceComponent: root.indeterminate || root.indeterminateAnimState !== CustomProgressBar.Stopped ? indeterminateComp : determinateComp
}
Component.onCompleted: updateIAnimState()
onIndeterminateChanged: updateIAnimState()
LinearIndicatorManager {
id: manager
gap: Appearance.spacing.extraSmall
Anim on completeEndProgress {
duration: manager.completeEndDuration
running: root.indeterminateAnimState === CustomProgressBar.Completing
to: 1
onFinished: {
if (root.indeterminateAnimState === CustomProgressBar.Completing)
root.indeterminateAnimState = CustomProgressBar.Stopped;
}
}
Anim on progress {
duration: manager.duration
easing.type: Easing.Linear
from: 0
loops: Animation.Infinite
running: root.indeterminateAnimState !== CustomProgressBar.Stopped
to: 1
}
}
Component {
id: determinateComp
Item {
Line {
id: remaining
anchors.right: parent.right
implicitWidth: parent.width - wave.implicitWidth - Appearance.spacing.extraSmall
}
Line {
property real implicitSize
anchors.right: parent.right
anchors.rightMargin: (parent.height - implicitHeight) / 2
anchors.verticalCenter: parent.verticalCenter
color: root.fgColor
implicitHeight: implicitSize
implicitWidth: implicitSize
radius: Appearance.rounding.full
Behavior on implicitSize {
Anim {
type: Anim.FastSpatial
}
}
Component.onCompleted: implicitSize = Qt.binding(() => parent.width - wave.implicitWidth < parent.height ? parent.height : 4)
}
Wave {
id: wave
anchors.left: parent.left
Behavior on implicitWidth {
Anim {
}
}
Component.onCompleted: implicitWidth = Qt.binding(() => parent.width * root.visualPosition)
}
}
}
Component {
id: indeterminateComp
Item {
id: content
Line {
bounds: {
const i = manager.activeIndicators[0]; // qmllint disable unresolved-type
return i ? root.toBounds(0, i.startFraction, i.gapSize / 2) : Qt.point(0, 0);
}
}
Line {
bounds: {
const i = manager.activeIndicators[manager.activeIndicators.length - 1]; // qmllint disable unresolved-type
return i ? root.toBounds(i.endFraction, 1, i.gapSize / 2) : Qt.point(0, 0);
}
}
Instantiator {
model: Math.max(manager.activeIndicators.length, 1) - 1 // qmllint disable unresolved-type
delegate: Line {
readonly property LinearIndicatorSegment cur: manager.activeIndicators[index] // qmllint disable unresolved-type
required property int index
readonly property LinearIndicatorSegment next: manager.activeIndicators[index + 1 % manager.activeIndicators.length] // qmllint disable unresolved-type
bounds: root.toBounds(cur.endFraction, next.startFraction, cur.gapSize / 2)
}
onObjectAdded: (_, obj) => content.data.push(obj)
onObjectRemoved: (_, obj) => {
const idx = content.data.indexOf(obj);
if (idx !== -1)
content.data.splice(idx, 1);
}
}
Instantiator {
model: manager.activeIndicators // qmllint disable unresolved-type
delegate: Wave {
readonly property point bounds: root.toBounds(modelData.startFraction, modelData.endFraction, modelData.gapSize / 2)
required property LinearIndicatorSegment modelData
color: root.fgColor
implicitWidth: bounds.y - bounds.x
x: bounds.x
}
onObjectAdded: (_, obj) => content.data.push(obj)
onObjectRemoved: (_, obj) => {
const idx = content.data.indexOf(obj);
if (idx !== -1)
content.data.splice(idx, 1);
}
}
}
}
component Line: CustomRect {
property point bounds
anchors.verticalCenter: parent.verticalCenter
color: root.bgColor
implicitHeight: parent.height
implicitWidth: bounds.y - bounds.x
radius: Appearance.rounding.full
x: bounds.x
}
component Wave: WavyLine {
id: wave
amplitudeMultiplier: root.wavy ? root.waveAmplitude : 0
anchors.verticalCenter: parent.verticalCenter
color: root.fgColor
frequency: root.waveFrequency
fullLength: parent.width
implicitHeight: lineWidth * amplitudeMultiplier * 2 + lineWidth
lineWidth: parent.height
startX: x
Behavior on amplitudeMultiplier {
Anim {
type: Anim.DefaultEffects
}
}
Behavior on color {
CAnim {
}
}
Anim on waveProgress {
duration: root.waveDuration
easing.type: Easing.Linear
from: 0
loops: Animation.Infinite
paused: wave.amplitudeMultiplier === 0 || root.wavePaused
running: true
to: 1
}
}
}
+4 -4
View File
@@ -33,13 +33,13 @@ RadioButton {
}
StateLayer {
function onClicked(): void {
root.click();
}
anchors.margins: -7
color: root.checked ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3primary
z: -1
onClicked: {
root.click();
}
}
CustomRect {
+106 -133
View File
@@ -1,49 +1,25 @@
import qs.Config
import QtQuick
import QtQuick.Templates
import qs.Helpers
import qs.Config
ScrollBar {
id: root
property bool _updatingFromFlickable: false
property bool _updatingFromUser: false
property bool animating
readonly property real effectiveSize: Math.max(nonAnimHeight, root.minimumSize)
readonly property real effectiveTravel: Math.max(0, 1 - root.effectiveSize)
required property Flickable flickable
property real nonAnimPosition
readonly property real nonAnimHeight: flickable.height / flickable.contentHeight
readonly property real nonAnimY: flickable.contentY / flickable.contentHeight
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 real travelScale: root.rawTravel > 0 ? root.effectiveTravel / root.rawTravel : 0
implicitWidth: 8
enabled: !Visibilities.getForActive().isDrawing
implicitWidth: Appearance.padding.extraSmall * 2
contentItem: CustomRect {
anchors.left: parent.left
anchors.right: parent.right
color: DynamicColors.palette.m3secondary
opacity: {
if (root.size === 1)
return 0;
if (fullMouse.pressed)
return 1;
if (mouse.containsMouse)
return 0.8;
if (root.policy === ScrollBar.AlwaysOn || root.shouldBeActive)
return 0.6;
return 0;
}
radius: Appearance.rounding.full
Behavior on opacity {
Anim {
}
}
MouseArea {
id: mouse
acceptedButtons: Qt.NoButton
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
}
contentItem: Item {
}
Behavior on position {
enabled: !fullMouse.pressed
@@ -52,58 +28,15 @@ ScrollBar {
}
}
Component.onCompleted: {
if (flickable) {
const contentHeight = flickable.contentHeight;
const height = flickable.height;
if (contentHeight > height) {
nonAnimPosition = Math.max(0, Math.min(1, flickable.contentY / (contentHeight - height)));
}
}
}
onHoveredChanged: {
if (hovered)
shouldBeActive = true;
shouldBeActive = hovered;
else
shouldBeActive = flickable.moving;
}
// Sync nonAnimPosition with Qt's automatic position binding
onPositionChanged: {
if (_updatingFromUser) {
_updatingFromUser = false;
return;
}
if (position === nonAnimPosition) {
animating = false;
return;
}
if (!animating && !_updatingFromFlickable && !fullMouse.pressed) {
nonAnimPosition = position;
}
}
// Sync nonAnimPosition with flickable when not animating
Connections {
function onContentYChanged() {
if (!animating && !fullMouse.pressed) {
_updatingFromFlickable = true;
const contentHeight = flickable.contentHeight;
const height = flickable.height;
if (contentHeight > height) {
nonAnimPosition = Math.max(0, Math.min(1, flickable.contentY / (contentHeight - height)));
} else {
nonAnimPosition = 0;
}
_updatingFromFlickable = false;
}
}
target: flickable
hideDelay.restart();
}
Connections {
function onMovingChanged(): void {
function onMovingChanged() {
if (root.flickable.moving)
root.shouldBeActive = true;
else
@@ -113,6 +46,57 @@ ScrollBar {
target: root.flickable
}
CustomClippingRect {
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.top: parent.top
implicitWidth: handle.implicitWidth
radius: Appearance.rounding.full
CustomRect {
id: handle
anchors.right: parent.right
color: DynamicColors.palette.m3secondary
implicitHeight: root.height * root.effectiveSize
implicitWidth: fullMouse.pressed || fullMouse.containsMouse ? Appearance.padding.extraSmall * 2 : Appearance.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: Appearance.rounding.full
y: root.reversed ? root.height * (1 + root.nonAnimY) * root.travelScale : root.height * root.nonAnimY * root.travelScale
Behavior on implicitWidth {
Anim {
}
}
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
MouseArea {
id: mouse
acceptedButtons: Qt.NoButton
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
}
}
}
Timer {
id: hideDelay
@@ -124,66 +108,55 @@ ScrollBar {
CustomMouseArea {
id: fullMouse
function onWheel(event: WheelEvent): void {
root.animating = true;
root._updatingFromUser = true;
let newPos = root.nonAnimPosition;
if (event.angleDelta.y > 0)
newPos = Math.max(0, root.nonAnimPosition - 0.1);
else if (event.angleDelta.y < 0)
newPos = Math.min(1 - root.size, root.nonAnimPosition + 0.1);
root.nonAnimPosition = newPos;
// Update flickable position
// Map scrollbar position [0, 1-size] to contentY [0, maxContentY]
if (root.flickable) {
const contentHeight = root.flickable.contentHeight;
const height = root.flickable.height;
if (contentHeight > height) {
const maxContentY = contentHeight - height;
const maxPos = 1 - root.size;
const contentY = maxPos > 0 ? (newPos / maxPos) * maxContentY : 0;
root.flickable.contentY = Math.max(0, Math.min(maxContentY, contentY));
}
}
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 updateFromEvent(event) {
var posInTrack = event.y / root.height;
var thumbTop = posInTrack - pressOffset;
thumbTop = Math.max(0, Math.min(root.effectiveTravel, thumbTop));
root.flickable.contentY = contentYFromThumbTop(thumbTop);
}
function visualThumbTop() {
const visualPos = root.reversed ? (1 + root.nonAnimY) : root.nonAnimY;
return visualPos * root.travelScale;
}
anchors.fill: parent
cursorShape: undefined
hoverEnabled: true
preventStealing: true
onPositionChanged: event => {
root._updatingFromUser = true;
const newPos = Math.max(0, Math.min(1 - root.size, event.y / root.height - root.size / 2));
root.nonAnimPosition = newPos;
// Update flickable position
// Map scrollbar position [0, 1-size] to contentY [0, maxContentY]
if (root.flickable) {
const contentHeight = root.flickable.contentHeight;
const height = root.flickable.height;
if (contentHeight > height) {
const maxContentY = contentHeight - height;
const maxPos = 1 - root.size;
const contentY = maxPos > 0 ? (newPos / maxPos) * maxContentY : 0;
root.flickable.contentY = Math.max(0, Math.min(maxContentY, contentY));
}
}
if (pressed)
updateFromEvent(event);
}
onPressed: event => {
root.animating = true;
root._updatingFromUser = true;
const newPos = Math.max(0, Math.min(1 - root.size, event.y / root.height - root.size / 2));
root.nonAnimPosition = newPos;
// Update flickable position
// Map scrollbar position [0, 1-size] to contentY [0, maxContentY]
if (root.flickable) {
const contentHeight = root.flickable.contentHeight;
const height = root.flickable.height;
if (contentHeight > height) {
const maxContentY = contentHeight - height;
const maxPos = 1 - root.size;
const contentY = maxPos > 0 ? (newPos / maxPos) * maxContentY : 0;
root.flickable.contentY = Math.max(0, Math.min(maxContentY, contentY));
}
var currentTop = visualThumbTop();
var currentBottom = currentTop + root.effectiveSize;
var clickPos = event.y / root.height;
var clickedInsideThumb = clickPos >= currentTop && clickPos <= currentBottom;
if (clickedInsideThumb) {
pressOffset = clickPos - currentTop;
} else {
pressOffset = 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;
}
}
}
+181 -31
View File
@@ -1,51 +1,201 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Templates
import ZShell.Components
import ZShell
import qs.Components
import qs.Config
Slider {
id: root
property color color: DynamicColors.palette.m3primary
property bool animateWave
property color bgColor: enabled ? DynamicColors.palette.m3secondaryContainer : Qt.alpha(DynamicColors.palette.m3onSurface, 0.1)
property color fgColor: enabled ? DynamicColors.palette.m3primary : Qt.alpha(DynamicColors.palette.m3onSurface, 0.38)
property real filledWidth
property alias inset: inset
property color insetColor: enabled ? (inset.attached ? DynamicColors.palette.m3onSecondaryContainer : DynamicColors.palette.m3onPrimary) : DynamicColors.palette.m3onSurface
property string insetIcon: ""
property real pos: visualPosition
property int radius: Appearance.rounding.small
property int waveDuration: 1000
property real waveFrequency: 6
property bool wavy
signal interaction(v: real)
implicitHeight: 12
implicitWidth: 200
contentItem: Item {
anchors.fill: parent
background: Item {
CustomRect {
anchors.bottom: parent.bottom
anchors.bottomMargin: root.implicitHeight / 6
anchors.left: parent.left
anchors.top: parent.top
anchors.topMargin: root.implicitHeight / 6
bottomRightRadius: root.implicitHeight / 6
color: root.color
implicitWidth: root.handle.x - root.implicitHeight / 6
radius: root.implicitHeight / 6
topRightRadius: root.implicitHeight / 6
id: remaining
anchors.left: handle.right
anchors.leftMargin: Appearance.spacing.extraSmall
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
bottomLeftRadius: Appearance.rounding.extraSmall / 2
color: root.bgColor
implicitHeight: parent.height * (parent.height <= 12 ? opacity : Math.min(opacity * 2, 1))
opacity: Math.min(width, 12) / 12
radius: root.radius
topLeftRadius: Appearance.rounding.extraSmall / 2
}
CustomRect {
anchors.bottom: parent.bottom
anchors.bottomMargin: root.implicitHeight / 6
anchors.right: parent.right
anchors.rightMargin: 4 * remaining.opacity
anchors.verticalCenter: parent.verticalCenter
color: root.fgColor
implicitHeight: 4 * remaining.opacity
implicitWidth: implicitHeight
opacity: remaining.opacity
radius: Appearance.rounding.full
}
CustomRect {
id: handle
anchors.left: filled.right
anchors.leftMargin: Appearance.spacing.extraSmall
anchors.verticalCenter: parent.verticalCenter
color: root.fgColor
implicitHeight: {
const t = ZUtils.clamp((parent.height - 12) / 16, 0, 1);
const lerp = (a, b) => a + (b - a) * t;
return parent.height * (mouse.pressed ? lerp(3.5, 1.5) : lerp(3, 1.2));
}
implicitWidth: 4
radius: Appearance.rounding.full
Behavior on implicitHeight {
Anim {
type: Anim.FastSpatial
}
}
}
Loader {
id: filled
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
asynchronous: true
sourceComponent: root.wavy ? waveComp : lineComp
}
MaterialIcon {
id: inset
readonly property bool attached: root.width ? (root.width - handle.implicitWidth - handle.anchors.leftMargin) * root.pos < inset.paintedWidth + Appearance.spacing.extraSmall * 2 : false
property real dockT: attached ? 1 : 0
anchors.bottom: parent.bottom
anchors.top: parent.top
anchors.topMargin: root.implicitHeight / 6
bottomLeftRadius: root.implicitHeight / 6
color: DynamicColors.tPalette.m3surfaceContainerHighest
implicitWidth: parent.width - root.handle.x - root.handle.implicitWidth - root.implicitHeight / 6
radius: root.implicitHeight / 6
topLeftRadius: root.implicitHeight / 6
color: root.insetColor
font.pixelSize: Appearance.font.size.smaller * 2
text: root.insetIcon
verticalAlignment: Text.AlignVCenter
visible: !root.wavy && root.insetIcon !== ""
x: Appearance.spacing.extraSmall + dockT * (handle.x + Appearance.spacing.small)
Behavior on dockT {
Anim {
}
}
}
Component {
id: lineComp
CustomRect {
bottomRightRadius: Appearance.rounding.extraSmall / 2
color: root.fgColor
implicitHeight: root.height
implicitWidth: root.filledWidth
radius: root.radius
topRightRadius: Appearance.rounding.extraSmall / 2
}
}
Component {
id: waveComp
WavyLine {
color: root.fgColor
frequency: root.waveFrequency
fullLength: root.width - handle.implicitWidth - handle.anchors.leftMargin
implicitHeight: lineWidth * amplitudeMultiplier * 2 + lineWidth
implicitWidth: root.filledWidth
lineWidth: root.height * 0.7
startX: x
Behavior on color {
CAnim {
}
}
Anim on waveProgress {
duration: root.waveDuration
easing.type: Easing.Linear
from: 0
loops: Animation.Infinite
paused: !root.animateWave
running: true
to: 1
}
}
}
}
handle: CustomRect {
anchors.verticalCenter: parent.verticalCenter
color: root.color
implicitHeight: root.implicitHeight
implicitWidth: root.implicitHeight / 4.5
radius: Appearance.rounding.full
x: root.visualPosition * root.availableWidth - implicitWidth / 2
Behavior on filledWidth {
id: widthBehavior
MouseArea {
acceptedButtons: Qt.NoButton
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
Anim {
}
}
Component.onCompleted: {
filledWidth = Qt.binding(() => (width - handle.implicitWidth - handle.anchors.leftMargin) * pos);
}
Binding {
id: posBinding
property: "pos"
target: root
value: ZUtils.clamp(mouse.pressStartPos + mouse.dragMovement, 0, 1)
when: mouse.pressed
}
MouseArea {
id: mouse
property real dragMovement
property real pressStartPos
property real pressStartX
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
implicitHeight: handle.implicitHeight
preventStealing: true
onPositionChanged: e => {
dragMovement = (e.x - pressStartX) / width;
root.interaction(posBinding.value);
}
onPressed: e => {
widthBehavior.enabled = false;
pressStartX = e.x;
pressStartPos = root.visualPosition;
}
onReleased: e => {
root.interaction(posBinding.value);
widthBehavior.enabled = true;
dragMovement = 0;
}
}
}
+6 -8
View File
@@ -94,7 +94,9 @@ RowLayout {
StateLayer {
id: upState
function onClicked(): void {
color: DynamicColors.palette.m3onPrimary
onClicked: {
let newValue = Math.min(root.max, root.value + root.step);
// Round to avoid floating point precision errors
const decimals = root.step < 1 ? Math.max(1, Math.ceil(-Math.log10(root.step))) : 0;
@@ -103,9 +105,6 @@ RowLayout {
root.displayText = newValue.toString();
root.valueModified(newValue);
}
color: DynamicColors.palette.m3onPrimary
onPressAndHold: timer.start()
onReleased: timer.stop()
}
@@ -128,7 +127,9 @@ RowLayout {
StateLayer {
id: downState
function onClicked(): void {
color: DynamicColors.palette.m3onPrimary
onClicked: {
let newValue = Math.max(root.min, root.value - root.step);
// Round to avoid floating point precision errors
const decimals = root.step < 1 ? Math.max(1, Math.ceil(-Math.log10(root.step))) : 0;
@@ -137,9 +138,6 @@ RowLayout {
root.displayText = newValue.toString();
root.valueModified(newValue);
}
color: DynamicColors.palette.m3onPrimary
onPressAndHold: timer.start()
onReleased: timer.stop()
}
+27 -57
View File
@@ -1,7 +1,6 @@
import QtQuick
import QtQuick.Layouts
import qs.Config
import qs.Helpers
Row {
id: root
@@ -13,61 +12,44 @@ Row {
property alias active: menu.active
property color color: type == CustomSplitButton.Filled ? DynamicColors.palette.m3primary : DynamicColors.palette.m3secondaryContainer
property bool disabled
property color disabledColor: Qt.alpha(DynamicColors.palette.m3onSurface, 0.1)
property color disabledTextColor: Qt.alpha(DynamicColors.palette.m3onSurface, 0.38)
readonly property alias expandBtn: expandBtn
property alias expanded: menu.expanded
property string fallbackIcon
property string fallbackText
property real horizontalPadding: Appearance.padding.normal
property alias iconLabel: iconLabel
property alias label: label
property alias menu: menu
property real horizontalPadding: Appearance.padding.larger
readonly property alias iconLabel: iconLabel
readonly property alias label: label
readonly property alias menu: menu
property alias menuItems: menu.items
property bool menuOnTop
property alias stateLayer: stateLayer
property real minLeftWidth
readonly property alias stateLayer: stateLayer
property color textColor: type == CustomSplitButton.Filled ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSecondaryContainer
readonly property alias textRow: textRow
property int type: CustomSplitButton.Filled
property real verticalPadding: Appearance.padding.smaller
property real verticalPadding: Appearance.padding.small
function closeDropdown(): void {
SettingsDropdowns.close(menu);
}
function openDropdown(): void {
SettingsDropdowns.open(menu, root);
}
function toggleDropdown(): void {
SettingsDropdowns.toggle(menu, root);
}
spacing: Math.floor(Appearance.spacing.small / 2)
onExpandedChanged: {
if (!expanded)
SettingsDropdowns.forget(menu);
}
spacing: Math.floor(Appearance.spacing.extraSmall)
CustomRect {
bottomRightRadius: Appearance.rounding.small / 2
color: !root.enabled ? root.disabledColor : root.color
implicitHeight: expandBtn.implicitHeight
implicitWidth: textRow.implicitWidth + root.horizontalPadding * 2
implicitWidth: Math.max(root.minLeftWidth, textRow.implicitWidth + root.horizontalPadding * 2)
radius: implicitHeight / 2 * Math.min(1, Appearance.rounding.scale)
topRightRadius: Appearance.rounding.small / 2
StateLayer {
id: stateLayer
function onClicked(): void {
root.active?.clicked();
}
bottomRightRadius: parent.bottomRightRadius
color: root.textColor
disabled: !root.enabled
rect.bottomRightRadius: parent.bottomRightRadius
rect.topRightRadius: parent.topRightRadius
enabled: root.enabled
topRightRadius: parent.topRightRadius
onClicked: root.active?.clicked()
}
RowLayout {
@@ -99,7 +81,7 @@ Row {
Behavior on Layout.preferredWidth {
Anim {
easing.bezierCurve: Appearance.anim.curves.emphasized
type: Anim.Emphasized
}
}
}
@@ -126,14 +108,12 @@ Row {
StateLayer {
id: expandStateLayer
function onClicked(): void {
root.toggleDropdown();
}
color: root.textColor
disabled: !root.enabled
enabled: root.enabled
rect.bottomLeftRadius: parent.bottomLeftRadius
rect.topLeftRadius: parent.topLeftRadius
onClicked: root.expanded = !root.expanded
}
MaterialIcon {
@@ -154,24 +134,14 @@ Row {
}
}
}
}
Menu {
id: menu
Menu {
id: menu
anchors.bottomMargin: Appearance.spacing.small
anchors.right: parent.right
anchors.top: parent.bottom
anchors.topMargin: Appearance.spacing.small
states: State {
when: root.menuOnTop
AnchorChanges {
anchors.bottom: expandBtn.top
anchors.top: undefined
target: menu
}
}
}
attachSideY: root.menuOnTop ? Menu.Top : Menu.Bottom
attachTo: expandBtn
marginY: Appearance.spacing.small * (root.menuOnTop ? -1 : 1)
thisSideY: root.menuOnTop ? Menu.Bottom : Menu.Top
}
}
+3 -6
View File
@@ -9,7 +9,6 @@ Item {
property alias active: splitButton.active
property alias buttonAlias: splitButton
property bool enabled: true
property alias expanded: splitButton.expanded
property int expandedZ: 100
required property string label
@@ -25,7 +24,7 @@ Item {
implicitHeight: row.implicitHeight + Appearance.padding.smaller * 2
opacity: shouldBeActive ? 1 : 0
scale: shouldBeActive ? 1 : 0.8
z: root.expanded ? expandedZ : -1
z: splitButton.menu.implicitHeight > 0 ? expandedZ : 1
Behavior on opacity {
Anim {
@@ -50,7 +49,6 @@ Item {
color: root.enabled ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.larger
text: root.label
z: root.expanded ? root.expandedZ : -1
}
CustomSplitButton {
@@ -58,14 +56,13 @@ Item {
enabled: root.enabled
type: CustomSplitButton.Filled
z: root.expanded ? root.expandedZ : -1
z: 2
menu.onItemSelected: item => {
root.selected(item);
splitButton.closeDropdown();
}
stateLayer.onClicked: {
splitButton.toggleDropdown();
splitButton.expanded = !splitButton.expanded;
}
}
}
+14 -12
View File
@@ -1,6 +1,6 @@
import QtQuick
import QtQuick.Templates
import QtQuick.Shapes
import QtQuick.Templates
import qs.Config
Switch {
@@ -13,26 +13,28 @@ Switch {
indicator: CustomRect {
color: root.checked && root.enabled ? DynamicColors.palette.m3primary : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, root.cLayer)
implicitHeight: 13 + 7 * 2
implicitHeight: Appearance.font.size.medium + Appearance.padding.normal * 2
implicitWidth: implicitHeight * 1.7
radius: Appearance.rounding.full
CustomRect {
readonly property real nonAnimWidth: root.pressed ? implicitHeight * 1.3 : implicitHeight
readonly property real nonAnimWidth: root.pressed ? implicitHeight * 1.2 : implicitHeight
anchors.verticalCenter: parent.verticalCenter
color: root.checked && root.enabled ? DynamicColors.palette.m3onPrimary : DynamicColors.layer(DynamicColors.palette.m3outline, root.cLayer + 1)
implicitHeight: parent.implicitHeight - 10
implicitHeight: parent.implicitHeight - Appearance.padding.extraSmall
implicitWidth: nonAnimWidth
radius: Appearance.rounding.full
x: root.checked ? parent.implicitWidth - nonAnimWidth - 10 / 2 : 10 / 2
x: root.checked ? parent.implicitWidth - nonAnimWidth - Appearance.padding.extraSmall / 2 : Appearance.padding.extraSmall / 2
Behavior on implicitWidth {
Anim {
type: Anim.FastSpatial
}
}
Behavior on x {
Anim {
type: Anim.FastSpatial
}
}
@@ -44,6 +46,7 @@ Switch {
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
}
@@ -63,14 +66,14 @@ Switch {
}
property point end2: {
if (root.pressed)
return Qt.point(width, height / 2);
return Qt.point(width * 0.8, height / 2);
if (root.checked)
return Qt.point(width * 0.85, height * 0.2);
return Qt.point(width * 0.85, height * 0.15);
}
property point start1: {
if (root.pressed)
return Qt.point(width * 0.1, height / 2);
return Qt.point(width * 0.2, height / 2);
if (root.checked)
return Qt.point(width * 0.15, height / 2);
return Qt.point(width * 0.15, height * 0.15);
@@ -88,7 +91,7 @@ Switch {
anchors.centerIn: parent
asynchronous: true
height: parent.implicitHeight - Appearance.padding.small * 2
height: parent.implicitHeight - Appearance.padding.larger
preferredRendererType: Shape.CurveRenderer
width: height
@@ -110,7 +113,7 @@ Switch {
}
ShapePath {
capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap
capStyle: ShapePath.RoundCap
fillColor: "transparent"
startX: icon.start1.x
startY: icon.start1.y
@@ -148,8 +151,7 @@ Switch {
}
component PropAnim: PropertyAnimation {
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
easing.type: Easing.BezierSpline
duration: Appearance.anim.durations.expressiveFastSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
}
}
+7 -6
View File
@@ -27,24 +27,25 @@ Text {
enabled: root.animate
SequentialAnimation {
Anim {
easing.bezierCurve: MaterialEasing.standardAccel
TAnim {
target: root
to: root.animateFrom
type: Anim.FastEffects
}
PropertyAction {
}
Anim {
easing.bezierCurve: MaterialEasing.standardDecel
TAnim {
target: root
to: root.animateTo
type: Anim.DefaultEffects
}
}
}
component Anim: NumberAnimation {
component TAnim: Anim {
duration: root.animateDuration / 2
easing.type: Easing.BezierSpline
properties: root.animateProp.split(",").length > 1 ? root.animateProp : ""
property: root.animateProp.split(",").length === 1 ? root.animateProp : ""
target: root
+2
View File
@@ -7,6 +7,8 @@ import qs.Config
TextField {
id: root
property int cursorHeight: root.height
background: null
color: DynamicColors.palette.m3onSurface
cursorVisible: !readOnly
+2 -1
View File
@@ -1,6 +1,6 @@
import qs.Config
import QtQuick
import QtQuick.Effects
import qs.Config
RectangularShadow {
property real dp: [0, 1, 3, 6, 8, 12][level]
@@ -13,6 +13,7 @@ RectangularShadow {
Behavior on dp {
Anim {
type: Anim.SlowEffects
}
}
}
+23 -56
View File
@@ -1,78 +1,45 @@
import qs.Config
import QtQuick
import qs.Config
CustomRect {
ButtonBase {
id: root
enum Type {
Filled,
Tonal,
Text
}
property color activeColour: type === IconButton.Filled ? DynamicColors.palette.m3primary : DynamicColors.palette.m3secondary
property color activeOnColour: type === IconButton.Filled ? DynamicColors.palette.m3onPrimary : type === IconButton.Tonal ? DynamicColors.palette.m3onSecondary : DynamicColors.palette.m3primary
property bool checked
property bool disabled
property color disabledColour: Qt.alpha(DynamicColors.palette.m3onSurface, 0.1)
property color disabledOnColour: Qt.alpha(DynamicColors.palette.m3onSurface, 0.38)
property alias font: label.font
property alias icon: label.text
property color inactiveColour: {
if (!toggle && type === IconButton.Filled)
readonly property alias label: label
activeColor: type === IconButton.Filled ? DynamicColors.palette.m3primary : DynamicColors.palette.m3secondary
activeOnColor: type === IconButton.Filled ? DynamicColors.palette.m3onPrimary : type === IconButton.Tonal ? DynamicColors.palette.m3onSecondary : DynamicColors.palette.m3primary
implicitHeight: {
const h = label.implicitHeight + padding * 2;
if (h % 2 !== 0)
return h + 1;
return h;
}
implicitWidth: implicitHeight
inactiveColor: {
if (!isToggle && type === IconButton.Filled)
return DynamicColors.palette.m3primary;
return type === IconButton.Filled ? DynamicColors.tPalette.m3surfaceContainer : DynamicColors.palette.m3secondaryContainer;
}
property color inactiveOnColour: {
if (!toggle && type === IconButton.Filled)
inactiveOnColor: {
if (!isToggle && type === IconButton.Filled)
return DynamicColors.palette.m3onPrimary;
return type === IconButton.Tonal ? DynamicColors.palette.m3onSecondaryContainer : DynamicColors.palette.m3onSurfaceVariant;
}
property bool internalChecked
property alias label: label
property real padding: type === IconButton.Text ? 10 / 2 : 7
property alias radiusAnim: radiusAnim
property alias stateLayer: stateLayer
property bool toggle
property int type: IconButton.Filled
signal clicked
color: type === IconButton.Text ? "transparent" : disabled ? disabledColour : internalChecked ? activeColour : inactiveColour
implicitHeight: label.implicitHeight + padding * 2
implicitWidth: implicitHeight
radius: internalChecked ? 6 : (implicitHeight / 2 * Math.min(1, 1)) * Appearance.rounding.scale
Behavior on radius {
Anim {
id: radiusAnim
}
}
onCheckedChanged: internalChecked = checked
StateLayer {
id: stateLayer
function onClicked(): void {
if (root.toggle)
root.internalChecked = !root.internalChecked;
root.clicked();
}
color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour
disabled: root.disabled
}
padding: type === IconButton.Text ? Appearance.padding.extraSmall / 2 : Appearance.padding.small
MaterialIcon {
id: label
anchors.centerIn: parent
color: root.disabled ? root.disabledOnColour : root.internalChecked ? root.activeOnColour : root.inactiveOnColour
fill: !root.toggle || root.internalChecked ? 1 : 0
anchors.verticalCenterOffset: 1
color: root.onColor
fill: !root.isToggle || root.internalChecked ? 1 : 0
font.pointSize: root.font.pointSize
Behavior on fill {
Anim {
type: Anim.DefaultEffects
}
}
}
+70
View File
@@ -0,0 +1,70 @@
import QtQuick
import QtQuick.Layouts
import qs.Config
ButtonBase {
id: root
property alias icon: iconLabel.text
readonly property alias iconLabel: iconLabel
readonly property alias label: label
property alias text: label.text
activeColor: type === TextButton.Filled ? DynamicColors.palette.m3primary : DynamicColors.palette.m3secondary
activeOnColor: {
if (type === TextButton.Text)
return DynamicColors.palette.m3primary;
return type === TextButton.Filled ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSecondary;
}
horizontalPadding: Appearance.padding.larger
implicitHeight: row.implicitHeight + verticalPadding * 2
implicitWidth: row.implicitWidth + horizontalPadding * 2
inactiveColor: {
if (!isToggle && type === TextButton.Filled)
return DynamicColors.palette.m3primary;
return type === TextButton.Filled ? DynamicColors.tPalette.m3surfaceContainer : DynamicColors.palette.m3secondaryContainer;
}
inactiveOnColor: {
if (!isToggle && type === TextButton.Filled)
return DynamicColors.palette.m3onPrimary;
if (type === TextButton.Text)
return DynamicColors.palette.m3primary;
return type === TextButton.Filled ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3onSecondaryContainer;
}
verticalPadding: Appearance.padding.small
RowLayout {
id: row
anchors.centerIn: parent
spacing: Appearance.spacing.small
MaterialIcon {
id: iconLabel
Layout.alignment: Qt.AlignVCenter
color: root.onColor
fill: root.internalChecked ? 1 : 0
font: {
const f = Qt.font(root.font);
f.pointSize = Math.round(root.font.pointSize * 1.2);
f.family = "Material Symbols Rounded";
return f;
}
Behavior on fill {
Anim {
type: Anim.DefaultEffects
}
}
}
CustomText {
id: label
Layout.alignment: Qt.AlignVCenter
color: root.onColor
font: root.font
}
}
}
+14 -3
View File
@@ -1,15 +1,26 @@
import qs.Config
CustomText {
property real fill
id: root
readonly property list<int> allowed: [20, 24, 40, 48]
property real fill: 0
property int grade: DynamicColors.light ? 0 : -25
readonly property int opsz: clampOpsz(optical)
property int optical: fontInfo.pixelSize
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: Appearance.font.size.larger
font.variableAxes: ({
FILL: fill.toFixed(1),
FILL: fill,
GRAD: grade,
opsz: fontInfo.pixelSize,
opsz: opsz,
wght: fontInfo.weight
})
}
+145 -69
View File
@@ -2,109 +2,185 @@ pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Config
import qs.Drawers
Elevation {
MouseArea {
id: root
enum Side {
Top,
Bottom,
Left,
Right
}
property MenuItem active: items[0] ?? null
property int attachSideX: Menu.Right
property int attachSideY: Menu.Bottom
required property Item attachTo
property bool expanded
property list<MenuItem> items
property real marginX
property real marginY
property int thisSideX: Menu.Right
property int thisSideY: Menu.Top
signal itemSelected(item: MenuItem)
implicitHeight: root.expanded ? column.implicitHeight + Appearance.padding.small * 2 : 0
implicitWidth: Math.max(200, column.implicitWidth)
level: 2
opacity: root.expanded ? 1 : 0
radius: Appearance.rounding.normal
Behavior on implicitHeight {
Anim {
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
anchors.fill: parent
cursorShape: undefined
enabled: expanded
layer.enabled: opacity < 1
opacity: expanded ? 1 : 0
parent: {
const win = QsWindow.window;
const contentWin = win as Windows;
return contentWin ? contentWin.interactionWrapper : (win as QsWindow).contentItem;
}
Behavior on opacity {
Anim {
duration: Appearance.anim.durations.expressiveDefaultSpatial
type: Anim.DefaultEffects
}
}
CustomClippingRect {
anchors.fill: parent
color: DynamicColors.palette.m3surfaceContainer
radius: parent.radius
onClicked: expanded = false
onExpandedChanged: {
const win = QsWindow.window;
const contentWin = win as Windows;
if (expanded) {
contentWin.menuRegion.x = menu.x;
contentWin.menuRegion.y = menu.y;
contentWin.menuRegion.width = menu.width;
contentWin.menuRegion.height = menu.height;
} else {
contentWin.menuRegion.x = 0;
contentWin.menuRegion.y = 0;
contentWin.menuRegion.width = 0;
contentWin.menuRegion.height = 0;
}
}
ColumnLayout {
id: column
TransformWatcher {
id: watcher
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 5
a: root.parent
b: root.attachTo
}
Repeater {
model: root.items
Elevation {
id: menu
CustomRect {
id: item
implicitHeight: column.implicitHeight + column.anchors.margins * 2
implicitWidth: Math.max(200, column.implicitWidth + column.anchors.margins * 2)
level: 2
radius: Appearance.rounding.medium
x: {
watcher.transform;
const item = root.attachTo;
let off = root.attachSideX === Menu.Left ? 0 : item.width;
if (root.thisSideX === Menu.Right)
off -= width;
return item.mapToItem(root.parent, off, 0).x + root.marginX;
}
y: {
watcher.transform;
const item = root.attachTo;
let off = root.attachSideY === Menu.Top ? 0 : item.height;
if (root.thisSideY === Menu.Bottom)
off -= height;
return item.mapToItem(root.parent, 0, off).y + root.marginY;
}
readonly property bool active: modelData === root.active
required property int index
required property MenuItem modelData
transform: Scale {
origin.y: root.thisSideY === Menu.Bottom ? menu.height : 0
yScale: root.expanded ? 1 : 0.1
Layout.fillWidth: true
implicitHeight: menuOptionRow.implicitHeight + Appearance.padding.normal * 2
implicitWidth: menuOptionRow.implicitWidth + Appearance.padding.normal * 2
Behavior on yScale {
Anim {
}
}
}
CustomRect {
anchors.fill: parent
color: DynamicColors.palette.m3surfaceContainerLow
radius: parent.radius
ColumnLayout {
id: column
anchors.fill: parent
anchors.margins: Appearance.padding.extraSmall
spacing: Appearance.spacing.extraSmall
Repeater {
id: repeater
model: root.items
CustomRect {
anchors.fill: parent
anchors.leftMargin: Appearance.padding.small
anchors.rightMargin: Appearance.padding.small
color: Qt.alpha(DynamicColors.palette.m3secondaryContainer, active ? 1 : 0)
radius: Appearance.rounding.normal - Appearance.padding.small
id: item
readonly property bool active: modelData === root.active
required property int index
required property MenuItem modelData
Layout.fillWidth: true
color: Qt.alpha(DynamicColors.palette.m3tertiaryContainer, active ? 1 : 0)
implicitHeight: menuOptionRow.implicitHeight + Appearance.padding.larger * 2
implicitWidth: menuOptionRow.implicitWidth + Appearance.padding.larger * 2
radius: Appearance.rounding.small
Behavior on radius {
Anim {
}
}
StateLayer {
function onClicked(): void {
color: item.active ? DynamicColors.palette.m3onTertiaryContainer : DynamicColors.palette.m3onSurface
enabled: root.expanded
onClicked: {
root.itemSelected(item.modelData);
root.active = item.modelData;
item.modelData.clicked();
root.expanded = false;
}
color: item.active ? DynamicColors.palette.m3onSecondaryContainer : DynamicColors.palette.m3onSurface
disabled: !root.expanded
}
}
RowLayout {
id: menuOptionRow
anchors.fill: parent
anchors.margins: Appearance.padding.normal
spacing: Appearance.spacing.small
MaterialIcon {
Layout.alignment: Qt.AlignVCenter
color: item.active ? DynamicColors.palette.m3onSecondaryContainer : DynamicColors.palette.m3onSurfaceVariant
text: item.modelData.icon
}
CustomText {
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
color: item.active ? DynamicColors.palette.m3onSecondaryContainer : DynamicColors.palette.m3onSurface
text: item.modelData.text
}
RowLayout {
id: menuOptionRow
Loader {
Layout.alignment: Qt.AlignVCenter
active: item.modelData.trailingIcon.length > 0
visible: active
anchors.fill: parent
anchors.margins: Appearance.padding.larger
spacing: Appearance.spacing.small
sourceComponent: MaterialIcon {
color: item.active ? DynamicColors.palette.m3onSecondaryContainer : DynamicColors.palette.m3onSurface
text: item.modelData.trailingIcon
MaterialIcon {
Layout.alignment: Qt.AlignVCenter
color: item.active ? DynamicColors.palette.m3onTertiaryContainer : DynamicColors.palette.m3onSurfaceVariant
text: item.modelData.icon
}
CustomText {
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
color: item.active ? DynamicColors.palette.m3onTertiaryContainer : DynamicColors.palette.m3onSurface
text: item.modelData.text
}
Loader {
Layout.alignment: Qt.AlignVCenter
active: item.modelData.trailingIcon.length > 0
asynchronous: true
visible: active
sourceComponent: MaterialIcon {
color: item.active ? DynamicColors.palette.m3onTertiaryContainer : DynamicColors.palette.m3onSurfaceVariant
text: item.modelData.trailingIcon
}
}
}
}
+173 -70
View File
@@ -1,95 +1,198 @@
import qs.Config
import QtQuick
import QtQuick.Shapes
import ZShell
import ZShell.Components
import qs.Helpers
import qs.Config
MouseArea {
id: root
property color color: DynamicColors.palette.m3onSurface
property bool disabled
property real radius: parent?.radius ?? 0
property alias rect: hoverLayer
property alias bottomLeftRadius: base.bottomLeftRadius
property alias bottomRightRadius: base.bottomRightRadius
property real circleRadius
property alias color: base.color
readonly property real endRadius: {
const d1 = distSq(0, 0);
const d2 = distSq(width, 0);
const d3 = distSq(0, height);
const d4 = distSq(width, height);
return (Math.sqrt(Math.max(d1, d2, d3, d4)) + (shapeMorph ? 24 : 0)) * 1.3;
}
property real endRadiusAtPress
property bool manualPressOverride
property real pressX: width / 2
property real pressY: height / 2
property alias radius: base.radius
readonly property alias rect: base
property bool shapeMorph
property bool showHoverBackground: true
property real stateOpacity: containsMouse ? 0.08 : 0
property alias topLeftRadius: base.topLeftRadius
property alias topRightRadius: base.topRightRadius
function onClicked(): void {
function clamp(r: real): real {
return Math.max(0, Math.min(r, width / 2, height / 2));
}
function distSq(x: real, y: real): real {
return (pressX - x) ** 2 + (pressY - y) ** 2;
}
function press(x: real, y: real): void {
pressX = x;
pressY = y;
fadeAnim.complete();
circleRadius = 0;
circle.opacity = 0.1;
rippleAnim.restart();
endRadiusAtPress = endRadius;
}
anchors.fill: parent
cursorShape: disabled ? undefined : Qt.PointingHandCursor
enabled: !disabled
cursorShape: !enabled ? undefined : Qt.PointingHandCursor
enabled: parent.enabled && !Visibilities.getForActive().isDrawing
hoverEnabled: true
onClicked: event => !disabled && onClicked(event)
onPressed: event => {
if (disabled)
return;
rippleAnim.x = event.x;
rippleAnim.y = event.y;
const dist = (ox, oy) => ox * ox + oy * oy;
rippleAnim.radius = Math.sqrt(Math.max(dist(event.x, event.y), dist(event.x, height - event.y), dist(width - event.x, event.y), dist(width - event.x, height - event.y)));
rippleAnim.restart();
Behavior on stateOpacity {
Anim {
type: Anim.DefaultEffects
}
}
SequentialAnimation {
onCircleRadiusChanged: {
if (!(pressed || manualPressOverride) && circleRadius > endRadiusAtPress * 0.99 && !fadeAnim.running)
fadeAnim.start();
}
onManualPressOverrideChanged: {
if (!(pressed || manualPressOverride) && circleRadius > endRadiusAtPress * 0.99 && !fadeAnim.running)
fadeAnim.start();
}
onPressed: e => press(e.x, e.y)
onPressedChanged: {
if (!(pressed || manualPressOverride) && !rippleAnim.running && circle.opacity > 0)
fadeAnim.start();
}
Anim {
id: rippleAnim
property real radius
property real x
property real y
PropertyAction {
property: "x"
target: ripple
value: rippleAnim.x
}
PropertyAction {
property: "y"
target: ripple
value: rippleAnim.y
}
PropertyAction {
property: "opacity"
target: ripple
value: 0.08
}
Anim {
easing.bezierCurve: MaterialEasing.standardDecel
from: 0
properties: "implicitWidth,implicitHeight"
target: ripple
to: rippleAnim.radius * 2
}
Anim {
property: "opacity"
target: ripple
to: 0
}
alwaysRunToEnd: true
duration: Appearance.anim.durations.expressiveSlowEffects * 2
easing.bezierCurve: Appearance.anim.curves.standard
property: "circleRadius"
target: root
to: root.endRadius
}
CustomClippingRect {
id: hoverLayer
Anim {
id: fadeAnim
property: "opacity"
target: circle
to: 0
type: Anim.SlowEffects
}
CustomRect {
id: base
anchors.fill: parent
border.pixelAligned: false
color: Qt.alpha(root.color, root.disabled ? 0 : root.pressed ? 0.1 : root.containsMouse ? 0.08 : 0)
radius: root.radius
bottomLeftRadius: root.parent?.bottomLeftRadius ?? radius ?? 0
bottomRightRadius: root.parent?.bottomRightRadius ?? radius ?? 0
color: DynamicColors.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
topLeftRadius: root.parent?.topLeftRadius ?? radius ?? 0
topRightRadius: root.parent?.topRightRadius ?? radius ?? 0
// qmllint enable missing-property
}
CustomRect {
id: ripple
Shape {
id: circle
border.pixelAligned: false
color: root.color
opacity: 0
radius: Appearance.rounding.full
anchors.fill: parent
opacity: 0
preferredRendererType: Shape.CurveRenderer
transform: Translate {
x: -ripple.width / 2
y: -ripple.height / 2
ShapePath {
fillColor: base.color
startX: root.clamp(base.topLeftRadius)
startY: 0
strokeColor: "transparent"
strokeWidth: 0
fillGradient: RadialGradient {
centerRadius: root.circleRadius
centerX: root.pressX
centerY: root.pressY
focalX: centerX
focalY: centerY
GradientStop {
color: Qt.alpha(base.color, 1)
position: 0
}
GradientStop {
color: Qt.alpha(base.color, 1)
position: ZUtils.clamp(1 - 0.2 * root.endRadius / root.circleRadius, 0.01, 0.99)
}
GradientStop {
color: Qt.alpha(base.color, ZUtils.clamp((root.circleRadius / root.endRadius - 0.9) / 0.1, 0, 1))
position: 1
}
}
PathLine {
x: root.width - root.clamp(base.topRightRadius)
y: 0
}
PathArc {
radiusX: root.clamp(base.topRightRadius)
radiusY: root.clamp(base.topRightRadius)
relativeX: root.clamp(base.topRightRadius)
relativeY: root.clamp(base.topRightRadius)
}
PathLine {
x: root.width
y: root.height - root.clamp(base.bottomRightRadius)
}
PathArc {
radiusX: root.clamp(base.bottomRightRadius)
radiusY: root.clamp(base.bottomRightRadius)
relativeX: -root.clamp(base.bottomRightRadius)
relativeY: root.clamp(base.bottomRightRadius)
}
PathLine {
x: root.clamp(base.bottomLeftRadius)
y: root.height
}
PathArc {
radiusX: root.clamp(base.bottomLeftRadius)
radiusY: root.clamp(base.bottomLeftRadius)
relativeX: -root.clamp(base.bottomLeftRadius)
relativeY: -root.clamp(base.bottomLeftRadius)
}
PathLine {
x: 0
y: root.clamp(base.topLeftRadius)
}
PathArc {
radiusX: root.clamp(base.topLeftRadius)
radiusY: root.clamp(base.topLeftRadius)
x: root.clamp(base.topLeftRadius)
y: 0
}
}
}
+40
View File
@@ -0,0 +1,40 @@
import QtQuick
import qs.Config
ButtonBase {
id: root
readonly property alias label: label
property alias text: label.text
activeColor: type === TextButton.Filled ? DynamicColors.palette.m3primary : DynamicColors.palette.m3secondary
activeOnColor: {
if (type === TextButton.Text)
return DynamicColors.palette.m3primary;
return type === TextButton.Filled ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSecondary;
}
horizontalPadding: Appearance.padding.normal
implicitHeight: label.implicitHeight + verticalPadding * 2
implicitWidth: label.implicitWidth + horizontalPadding * 2
inactiveColor: {
if (!isToggle && type === TextButton.Filled)
return DynamicColors.palette.m3primary;
return type === TextButton.Filled ? DynamicColors.tPalette.m3surfaceContainer : DynamicColors.palette.m3secondaryContainer;
}
inactiveOnColor: {
if (!isToggle && type === TextButton.Filled)
return DynamicColors.palette.m3onPrimary;
if (type === TextButton.Text)
return DynamicColors.palette.m3primary;
return type === TextButton.Filled ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3onSecondaryContainer;
}
verticalPadding: Appearance.padding.small
CustomText {
id: label
anchors.centerIn: parent
color: root.onColor
font: root.font
}
}
-2
View File
@@ -7,8 +7,6 @@ Singleton {
readonly property AppearanceConf.Deform deform: Config.appearance.deform
readonly property AppearanceConf.FontStuff font: Config.appearance.font
readonly property AppearanceConf.Padding padding: Config.appearance.padding
// Literally just here to shorten accessing stuff :woe:
// Also kinda so I can keep accessing it with `Appearance.xxx` instead of `Conf.appearance.xxx`
readonly property AppearanceConf.Rounding rounding: Config.appearance.rounding
readonly property AppearanceConf.Spacing spacing: Config.appearance.spacing
readonly property AppearanceConf.Transparency transparency: Config.appearance.transparency
+18 -4
View File
@@ -28,9 +28,13 @@ JsonObject {
property list<real> emphasized: [0.05, 0, 2 / 15, 0.06, 1 / 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1]
property list<real> emphasizedAccel: [0.3, 0, 0.8, 0.15, 1, 1]
property list<real> emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1]
property list<real> expressiveDefaultEffects: [0.34, 0.8, 0.34, 1, 1, 1]
property list<real> expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1]
property list<real> expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1]
property list<real> expressiveFastEffects: [0.31, 0.94, 0.34, 1, 1, 1]
property list<real> expressiveFastSpatial: [0.42, 1.67, 0.21, 0.9, 1, 1]
property list<real> expressiveSlowEffects: [0.34, 0.88, 0.34, 1, 1, 1]
property list<real> expressiveSlowSpatial: [0.39, 1.29, 0.35, 0.98, 1, 1]
property list<real> standard: [0.2, 0, 0, 1, 1, 1]
property list<real> standardAccel: [0.3, 0, 1, 1, 1, 1]
property list<real> standardDecel: [0, 0, 0, 1, 1, 1]
@@ -38,7 +42,9 @@ JsonObject {
component AnimDurations: JsonObject {
property int expressiveDefaultSpatial: 500 * scale
property int expressiveEffects: 200 * scale
property int expressiveFastEffects: 150 * scale
property int expressiveFastSpatial: 350 * scale
property int expressiveSlowEffects: 300 * scale
property int extraLarge: 1000 * scale
property int large: 600 * scale
property int normal: 400 * scale
@@ -57,7 +63,8 @@ JsonObject {
component FontSize: JsonObject {
property int extraLarge: 28 * scale
property int large: 18 * scale
property int larger: 15 * scale
property int larger: 16 * scale
property int medium: 14 * scale
property int normal: 13 * scale
property real scale: 1
property int small: 11 * scale
@@ -70,23 +77,30 @@ JsonObject {
}
}
component Padding: JsonObject {
property int large: 15 * scale
property int larger: 13 * scale
property int normal: 9 * scale
property int extraLarge: 28 * scale
property int extraLargeIncreased: 32 * scale
property int extraSmall: 4 * scale
property int large: 16 * scale
property int largeIncreased: 20 * scale
property int larger: 12 * scale
property int normal: 8 * scale
property real scale: 1
property int small: 5 * scale
property int smaller: 7 * scale
property int smallest: 2 * scale
}
component Rounding: JsonObject {
property int extraSmall: 4 * scale
property int full: 1000 * scale
property int large: 24 * scale
property int medium: 16 * scale
property int normal: 18 * scale
property real scale: 1
property int small: 12 * scale
property int smallest: 8 * scale
}
component Spacing: JsonObject {
property int extraSmall: 4 * scale
property int large: 20 * scale
property int larger: 16 * scale
property int normal: 12 * scale
+15
View File
@@ -0,0 +1,15 @@
import Quickshell.Io
JsonObject {
property bool enabled: true
property int maxEntriesShown: 10
property Sizes sizes: Sizes {
}
component Sizes: JsonObject {
property int itemHeight: 60
property int minPreviewWidth: 200
property int previewWidth: 500
property int width: 500
}
}
+21 -2
View File
@@ -13,6 +13,7 @@ Singleton {
property alias appearance: adapter.appearance
property alias background: adapter.background
property alias barConfig: adapter.barConfig
property alias clipboard: adapter.clipboard
property alias colors: adapter.colors
property alias dashboard: adapter.dashboard
property alias dock: adapter.dock
@@ -116,6 +117,19 @@ Singleton {
};
}
function serializeClipboard(): var {
return {
enabled: clipboard.enabled,
maxEntriesShown: clipboard.maxEntriesShown,
sizes: {
width: clipboard.sizes.width,
previewWidth: clipboard.sizes.previewWidth,
minPreviewWidth: clipboard.sizes.minPreviewWidth,
itemHeight: clipboard.sizes.itemHeight
}
};
}
function serializeColors(): var {
return {
schemeType: colors.schemeType,
@@ -143,7 +157,8 @@ Singleton {
launcher: serializeLauncher(),
colors: serializeColors(),
dock: serializeDock(),
screenshot: serializeScreenshot()
screenshot: serializeScreenshot(),
clipboard: serializeClipboard()
};
}
@@ -173,7 +188,7 @@ Singleton {
resourceProgessThickness: dashboard.sizes.resourceProgessThickness,
weatherWidth: dashboard.sizes.weatherWidth,
mediaCoverArtSize: dashboard.sizes.mediaCoverArtSize,
mediaVisualiserSize: dashboard.sizes.mediaVisualiserSize,
mediaVisualizerSize: dashboard.sizes.mediaVisualizerSize,
resourceSize: dashboard.sizes.resourceSize
}
};
@@ -194,6 +209,7 @@ Singleton {
return {
logo: general.logo,
wallpaperPath: general.wallpaperPath,
showOverFullscreen: general.showOverFullscreen,
desktopIcons: general.desktopIcons,
dateFormat: general.dateFormat,
color: {
@@ -315,6 +331,7 @@ Singleton {
weatherLocation: services.weatherLocation,
updates: services.updates,
useFahrenheit: services.useFahrenheit,
minBrightness: services.minBrightness,
ddcutilService: services.ddcutilService,
useTwelveHourClock: services.useTwelveHourClock,
gpuType: services.gpuType,
@@ -447,6 +464,8 @@ Singleton {
}
property BarConfig barConfig: BarConfig {
}
property ClipboardConfig clipboard: ClipboardConfig {
}
property Colors colors: Colors {
}
property DashboardConfig dashboard: DashboardConfig {
+1 -1
View File
@@ -25,7 +25,7 @@ JsonObject {
readonly property int mediaCoverArtSize: 150
readonly property int mediaProgressSweep: 180
readonly property int mediaProgressThickness: 8
readonly property int mediaVisualiserSize: 80
readonly property int mediaVisualizerSize: 200
readonly property int mediaWidth: 200
readonly property int resourceProgessThickness: 10
readonly property int resourceSize: 200
+5
View File
@@ -13,11 +13,16 @@ JsonObject {
property Idle idle: Idle {
}
property string logo: ""
property bool showOverFullscreen: true
property string wallpaperPath: Quickshell.env("HOME") + "/Pictures/Wallpapers"
component Apps: JsonObject {
property list<string> archiver: ["ark"]
property list<string> audio: ["pavucontrol"]
property list<string> document: ["libreoffice"]
property list<string> editor: ["kate"]
property list<string> explorer: ["dolphin"]
property list<string> image: ["imv"]
property list<string> playback: ["mpv"]
property list<string> terminal: ["kitty"]
}
+1
View File
@@ -8,6 +8,7 @@ JsonObject {
property string defaultPlayer: "Spotify"
property string gpuType: ""
property real maxVolume: 1.0
property real minBrightness: 0.01
property list<var> playerAliases: [
{
"from": "com.github.th_ch.youtube_music",
+6
View File
@@ -215,6 +215,10 @@ Singleton {
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;
@@ -237,6 +241,7 @@ Singleton {
}
property real expireTimeout: 5
property bool hasActionIcons
property var hints
property string image
property string imageSource
property var locks: new Set()
@@ -333,6 +338,7 @@ Singleton {
appName = notification.appName;
imageSource = notification.image || "";
image = imageSource;
hints = notification.hints;
expireTimeout = notification.expireTimeout;
urgency = notification.urgency;
resident = notification.resident;
+25
View File
@@ -0,0 +1,25 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
Variants {
model: Quickshell.screens
Scope {
id: scope
required property ShellScreen modelData
Exclusions {
bar: content.bar
screen: scope.modelData
}
Windows {
id: content
screen: scope.modelData
}
}
}
+21 -27
View File
@@ -1,36 +1,30 @@
import QtQuick
pragma ComponentBehavior: Bound
Canvas {
import Quickshell
import QtQuick
import ZShell.Internal
Item {
id: root
property color penColor: "white"
property real penWidth: 4
property var points: []
readonly property alias content: contentLoader.item
readonly property PersistentProperties drawingState: PersistentProperties {
property color penColor: "white"
property int penWidth: 4
function clear(): void {
var ctx = getContext('2d');
root.points = [];
ctx.reset();
root.requestPaint();
reloadableId: "drawingState"
}
required property PersistentProperties visibilities
renderStrategy: Canvas.Cooperative
Loader {
id: contentLoader
onPaint: {
if (points.length < 2)
return;
var ctx = root.getContext('2d');
ctx.save();
ctx.lineWidth = root.penWidth;
ctx.strokeStyle = root.penColor;
ctx.lineJoin = "round";
ctx.lineCap = "round";
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
for (var i = 1; i < points.length; i++)
ctx.lineTo(points[i].x, points[i].y);
ctx.stroke();
points = points.slice(points.length - 2);
ctx.restore();
active: root.visibilities.isDrawing
anchors.fill: parent
sourceComponent: StrokeCanvas {
penColor: root.drawingState.penColor
penWidth: root.drawingState.penWidth
}
}
}
-60
View File
@@ -1,60 +0,0 @@
import Quickshell
import QtQuick
import qs.Components
import qs.Config
CustomMouseArea {
id: root
required property var bar
required property Drawing drawing
required property Panels panels
required property var popout
required property PersistentProperties visibilities
function inLeftPanel(panel: Item, x: real, y: real): bool {
return x < panel.x + panel.width + Config.barConfig.border && withinPanelHeight(panel, x, y);
}
function withinPanelHeight(panel: Item, x: real, y: real): bool {
const panelY = panel.y + bar.implicitHeight;
return y >= panelY && y <= panelY + panel.height;
}
acceptedButtons: Qt.LeftButton | Qt.RightButton
enabled: z > 0
hoverEnabled: true
visible: root.visibilities.isDrawing
onPositionChanged: event => {
const x = event.x;
const y = event.y;
if (root.visibilities.isDrawing && (event.buttons & Qt.LeftButton)) {
root.drawing.points.push(Qt.point(x, y));
root.drawing.requestPaint();
return;
}
if (!(event.buttons & Qt.LeftButton) && root.inLeftPanel(root.popout, x, y)) {
root.z = -2;
root.panels.drawing.expanded = true;
}
}
onPressed: event => {
const x = event.x;
const y = event.y;
if (root.visibilities.isDrawing && (event.buttons & Qt.LeftButton)) {
root.panels.drawing.expanded = false;
root.drawing.points.push(Qt.point(x, y));
root.drawing.requestPaint();
return;
}
if (event.buttons & Qt.RightButton)
root.drawing.clear();
}
onReleased: {
root.drawing.points = [];
}
}
+61 -8
View File
@@ -9,9 +9,9 @@ Item {
id: root
required property Item bar
required property real borderThickness
property bool dashboardShortcutActive
required property Drawing drawing
required property DrawingInput input
property bool osdShortcutActive
required property Panels panels
required property BarPopouts.Wrapper popouts
@@ -48,7 +48,7 @@ Item {
}
function withinPanelWidth(panel: Item, x: real, y: real): bool {
const panelX = panel.x;
const panelX = panel.x + root.borderThickness;
return x >= panelX && x <= panelX + panel.width;
}
@@ -59,7 +59,8 @@ Item {
cursorShape: (active && centroid.pressPosition.y < root.bar.implicitHeight) ? Qt.ClosedHandCursor : undefined
dragThreshold: 0
grabPermissions: PointerHandler.CanTakeOverFromHandlersOfDifferentType | PointerHandler.ApprovesTakeOverByAnything
enabled: !root.visibilities.isDrawing
grabPermissions: PointerHandler.CanTakeOverFromHandlersOfSameType | PointerHandler.ApprovesTakeOverByAnything
maximumPointCount: 1
minimumPointCount: 1
target: null
@@ -74,7 +75,7 @@ Item {
const dragX = x - centroid.pressPosition.x;
const dragY = y - centroid.pressPosition.y;
if (centroid.pressPosition.y >= root.screen.height - Config.barConfig.border && dragY < -200)
if (centroid.pressPosition.y >= root.screen.height - Config.barConfig.border && centroid.pressPosition.x > root.screen.width / 5 && dragY < -200)
root.visibilities.launcher = true;
if (root.singleGestureTriggered)
@@ -90,7 +91,10 @@ Item {
}
}
if (!Config.dock.hoverToReveal && centroid.pressPosition.y > root.screen.height - root.bar.implicitHeight)
if (centroid.pressPosition.y > root.screen.height - Config.barConfig.border && centroid.pressPosition.x < root.screen.width / 5 && dragY < -50)
root.visibilities.clipboard = true;
if (!Config.dock.hoverToReveal && centroid.pressPosition.y > root.screen.height - root.bar.implicitHeight && centroid.pressPosition.x > root.screen.width / 5 && !root.visibilities.launcher)
if (dragY < -10) {
root.visibilities.dock = true;
root.singleGestureTriggered = true;
@@ -113,9 +117,52 @@ Item {
}
}
PointHandler {
id: drawingHandler
property bool setInitialPoint: false
acceptedButtons: Qt.LeftButton | Qt.RightButton
enabled: root.visibilities.isDrawing && (!root.inLeftPanel(root.panels.drawing, hoverHandler.point.position.x, hoverHandler.point.position.y) || !root.panels.drawing.expanded)
onActiveChanged: {
if (!active) {
setInitialPoint = false;
root.drawing.content.endStroke();
} else {
root.panels.drawing.collapse();
}
}
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) {
root.drawing.content.clear();
return;
}
if (x === 0 && y === 0 && origX === 0 && origY === 0)
return;
if (!setInitialPoint) {
setInitialPoint = true;
root.drawing.content.beginStroke(origX, origY);
return;
}
root.drawing.content.appendPoint(x, y);
}
}
HoverHandler {
id: hoverHandler
cursorShape: root.visibilities.isDrawing && !root.inLeftPanel(root.panels.drawing, point.position.x, point.position.y) ? Qt.BlankCursor : undefined
onHoveredChanged: {
if (!hovered) {
if (!root.osdShortcutActive) {
@@ -135,9 +182,15 @@ Item {
const x = point.position.x;
const y = point.position.y;
if (root.visibilities.isDrawing && !root.inLeftPanel(root.panels.drawing, x, y)) {
root.input.z = 2;
root.panels.drawing.expanded = false;
if (root.visibilities.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.visibilities.bar && Config.barConfig.autoHide && y < root.bar.implicitHeight)
+15 -2
View File
@@ -13,17 +13,20 @@ import qs.Modules.Resources as Resources
import qs.Modules.Settings as Settings
import qs.Modules.Drawing as Drawing
import qs.Modules.Dock as Dock
import qs.Modules.Clipboard as Clipboard
import qs.Config
Item {
id: root
required property Item bar
required property real borderThickness
readonly property alias clipboard: clipboard
readonly property alias dashboard: dashboard
readonly property alias dashboardWrapper: dashboardWrapper
readonly property alias dock: dock
readonly property alias drawing: drawing
required property Canvas drawingItem
required property var drawingItem
readonly property alias launcher: launcher
readonly property alias notifications: notifications
readonly property alias osd: osd
@@ -41,7 +44,7 @@ Item {
required property PersistentProperties visibilities
anchors.fill: parent
anchors.margins: Config.barConfig.border
anchors.margins: borderThickness
anchors.topMargin: bar.implicitHeight
Item {
@@ -97,6 +100,7 @@ Item {
id: popouts
anchors.top: parent.top
borderThickness: root.borderThickness
screen: root.screen
}
@@ -207,4 +211,13 @@ Item {
screen: root.screen
visibilities: root.visibilities
}
Clipboard.Wrapper {
id: clipboard
anchors.bottom: parent.bottom
anchors.left: parent.left
screen: root.screen
visibilities: root.visibilities
}
}
+422 -359
View File
@@ -9,397 +9,460 @@ import Quickshell.Hyprland
import ZShell.Blobs
import qs.Daemons
import qs.Components
import qs.Modules
import qs.Modules.Bar
import qs.Config
import qs.Helpers
import qs.Drawers
Variants {
model: Quickshell.screens
CustomWindow {
id: root
Scope {
id: scope
readonly property alias bar: bar
readonly property real borderLayoutThickness: hasFullscreen ? 0 : Config.barConfig.border
readonly property real borderRounding: Config.barConfig.rounding * (1 - fsTransitionProg)
readonly property real borderThickness: Config.barConfig.border * (1 - fsTransitionProg)
readonly property int dragMaskPadding: {
if (focusGrab.active)
return 0;
required property var modelData
if (monitor?.lastIpcObject.specialWorkspace?.name || monitor?.activeWorkspace.lastIpcObject.windows > 0)
return 0;
Exclusions {
bar: bar
screen: scope.modelData
return 100;
}
property real fsTransitionProg: hasFullscreen ? 1 : 0
readonly property bool hasFullscreen: {
if (hasSpecialWorkspace) {
const specialName = monitor?.lastIpcObject.specialWorkspace?.name;
if (!specialName)
return false;
const specialWs = Hypr.workspaces.values.find(ws => ws.name === specialName);
return specialWs?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false;
}
return hasFullscreenOnNormalWs;
}
readonly property bool hasFullscreenOnNormalWs: monitor?.activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false
readonly property bool hasSpecialWorkspace: (monitor?.lastIpcObject.specialWorkspace?.name.length ?? 0) > 0
readonly property alias interactionWrapper: interactions
readonly property alias menuRegion: menuPopoutRegion
readonly property HyprlandMonitor monitor: Hypr.monitorFor(screen)
property var root: Quickshell.shellDir
readonly property real sdfBorderOffset: 2 * fsTransitionProg
readonly property real shadowOpacity: 0.7 * (1 - fsTransitionProg)
property color surfaceColor: DynamicColors.tPalette.m3surface
WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.settings ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
WlrLayershell.layer: (fsTransitionProg > 0 && Config.general.showOverFullscreen) || (hasSpecialWorkspace && hasFullscreenOnNormalWs) ? WlrLayer.Overlay : WlrLayer.Top
color: "transparent"
contentItem.focus: true
mask: visibilities.isDrawing ? null : (hasFullscreen ? emptyRegion : region)
name: "Bar"
Behavior on fsTransitionProg {
Anim {
}
}
Behavior on surfaceColor {
CAnim {
}
}
contentItem.Keys.onEscapePressed: {
if (Config.barConfig.autoHide)
visibilities.bar = false;
visibilities.launcher = false;
visibilities.sidebar = false;
visibilities.dashboard = false;
visibilities.osd = false;
visibilities.settings = false;
visibilities.resources = false;
visibilities.dock = false;
visibilities.clipboard = false;
panels.popouts.hasCurrent = false;
}
onHasFullscreenChanged: {
visibilities.launcher = false;
visibilities.sidebar = false;
visibilities.dashboard = false;
visibilities.osd = false;
visibilities.settings = false;
visibilities.resources = false;
visibilities.clipboard = false;
visibilities.dock = false;
panels.popouts.hasCurrent = false;
}
Region {
id: menuPopoutRegion
intersection: Intersection.Subtract
}
Region {
id: emptyRegion
height: panels.notifications.height
width: panels.notifications.width
x: panels.notifications.x + root.borderThickness
y: panels.notifications.y + bar.implicitHeight
}
Region {
id: region
height: root.height - bar.implicitHeight - root.borderThickness - root.dragMaskPadding * 2
intersection: Intersection.Xor
regions: [...popoutRegions.instances, menuPopoutRegion]
width: root.width - root.borderThickness * 2 - root.dragMaskPadding * 2
x: root.borderThickness + root.dragMaskPadding
y: bar.implicitHeight + root.dragMaskPadding
}
anchors {
bottom: true
left: true
right: true
top: true
}
Variants {
id: popoutRegions
model: panels.children
Region {
required property Item modelData
height: modelData.height
intersection: Intersection.Subtract
width: modelData.width
x: modelData.x + root.borderThickness
y: modelData.y + bar.implicitHeight
}
}
HyprlandFocusGrab {
id: focusGrab
active: visibilities.dock || visibilities.resources || visibilities.launcher || visibilities.sidebar || visibilities.dashboard || visibilities.settings || visibilities.clipboard || (panels.popouts.hasCurrent && panels.popouts.currentName.startsWith("traymenu"))
windows: [root]
onCleared: {
visibilities.launcher = false;
visibilities.sidebar = false;
visibilities.dashboard = false;
visibilities.osd = false;
visibilities.settings = false;
visibilities.resources = false;
visibilities.clipboard = false;
visibilities.dock = false;
panels.popouts.hasCurrent = false;
}
}
PersistentProperties {
id: visibilities
property bool bar
property bool clipboard
property bool dashboard
property bool dock
property bool isDrawing
property bool launcher
property bool notif: NotifServer.popups.length > 0
property bool osd
property bool resources
property bool settings
property bool sidebar
Component.onCompleted: Visibilities.load(root.screen, this)
}
IpcHandler {
function toggleLauncher(fix: string): void {
visibilities.launcher = !visibilities.launcher;
}
CustomWindow {
id: win
target: "visibilities"
}
readonly property bool hasFullscreen: Hypr.monitorFor(screen)?.activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen === 2)
property var root: Quickshell.shellDir
Binding {
property: "bar"
target: visibilities
value: visibilities.sidebar || visibilities.dashboard || visibilities.osd || (!Config.barConfig.hideWhenNotif && visibilities.notif) || visibilities.resources || visibilities.settings || bar.isHovered
when: Config.barConfig.autoHide
}
WlrLayershell.exclusionMode: ExclusionMode.Ignore
// WlrLayershell.keyboardFocus: visibilities.dock || visibilities.launcher || visibilities.sidebar || visibilities.dashboard || visibilities.settings || visibilities.resources ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
color: "transparent"
contentItem.focus: true
mask: visibilities.isDrawing ? null : region
name: "Bar"
screen: scope.modelData
Item {
id: surface
contentItem.Keys.onEscapePressed: {
if (Config.barConfig.autoHide)
visibilities.bar = false;
visibilities.sidebar = false;
visibilities.dashboard = false;
visibilities.osd = false;
visibilities.settings = false;
visibilities.resources = false;
}
onHasFullscreenChanged: {
visibilities.launcher = false;
visibilities.dashboard = false;
visibilities.osd = false;
visibilities.settings = false;
visibilities.resources = false;
}
anchors.fill: parent
layer.enabled: true
opacity: root.surfaceColor.a
Region {
id: region
layer.effect: MultiEffect {
blurMax: 32
shadowColor: Qt.alpha(DynamicColors.palette.m3shadow, Math.max(0, root.shadowOpacity))
shadowEnabled: true
}
height: win.height - bar.implicitHeight - Config.barConfig.border
intersection: Intersection.Xor
regions: popoutRegions.instances
width: win.width - Config.barConfig.border * 2
x: Config.barConfig.border
y: bar.implicitHeight
}
BlobGroup {
id: blobGroup
anchors {
bottom: true
left: true
right: true
top: true
}
color: root.surfaceColor
smoothing: Config.barConfig.smoothing
}
Variants {
id: popoutRegions
BlobInvertedRect {
anchors.fill: parent
anchors.margins: -50
borderBottom: root.borderThickness - anchors.margins - root.sdfBorderOffset
borderLeft: root.borderThickness - anchors.margins - root.sdfBorderOffset
borderRight: root.borderThickness - anchors.margins - root.sdfBorderOffset
borderTop: bar.implicitHeight - anchors.margins - root.sdfBorderOffset
group: blobGroup
radius: root.borderRounding
}
model: panels.children
PanelBg {
id: dashBg
Region {
required property Item modelData
property real extraHeight: 0.2
height: modelData.height
intersection: Intersection.Subtract
width: modelData.width
x: modelData.x + Config.barConfig.border
y: modelData.y + bar.implicitHeight
deformAmount: 0.06
implicitHeight: panels.dashboard.height * (1 + extraHeight)
implicitWidth: panels.dashboard.width
panel: panels.dashboardWrapper
radius: Appearance.rounding.normal
x: panels.dashboardWrapper.x + panels.dashboard.x + root.borderThickness
y: panels.dashboardWrapper.y + panels.dashboard.y + bar.implicitHeight - panels.dashboard.height * extraHeight
}
PanelBg {
id: launcherBg
property real extraHeight: 0.2
deformAmount: 0.06
implicitHeight: panels.launcher.height * (1 + extraHeight)
panel: panels.launcher
radius: Appearance.rounding.smallest + 5
y: panels.launcher.y + bar.implicitHeight
}
PanelBg {
id: sidebarBg
bottomLeftRadius: 0
deformAmount: 0.04
exclude: panels.sidebar.offsetScale > 0.08 ? [] : [utilsBg]
implicitHeight: panel.height * (1 / rawDeformMatrix.m22) + 2
panel: panels.sidebar
}
PanelBg {
id: osdBg
deformAmount: 0.1
implicitHeight: panels.osd.height
implicitWidth: panels.osd.width
panel: panels.osdWrapper
radius: 20
x: panels.osdWrapper.x + panels.osd.x + root.borderThickness
y: panels.osdWrapper.y + panels.osd.y + bar.implicitHeight
}
PanelBg {
id: notifsBg
panel: panels.notifications
radius: Appearance.rounding.normal
}
PanelBg {
id: utilsBg
deformAmount: 0.1
exclude: panels.sidebar.offsetScale > 0.08 ? [] : [sidebarBg]
panel: panels.utilities
topLeftRadius: 0
}
PanelBg {
id: popoutBg
property real extraHeight: 0.2
deformAmount: panels.popouts.currentName.startsWith("traymenu") ? 0.15 : 0.08
implicitHeight: panels.popouts.height * (1 + extraHeight)
implicitWidth: panels.popouts.width
panel: panels.popoutsWrapper
radius: panels.popouts.current?.panelRadius ?? Appearance.rounding.normal
x: panels.popoutsWrapper.x + panels.popouts.x + root.borderThickness
y: panels.popoutsWrapper.y + panels.popouts.y + bar.implicitHeight - panels.popouts.height * extraHeight
Behavior on extraHeight {
Anim {
}
}
}
HyprlandFocusGrab {
id: focusGrab
PanelBg {
id: resourcesBg
active: visibilities.dock || visibilities.resources || visibilities.launcher || visibilities.sidebar || visibilities.dashboard || visibilities.settings || (panels.popouts.hasCurrent && panels.popouts.currentName.startsWith("traymenu"))
windows: [win]
deformAmount: 0.05
implicitHeight: panels.resources.height
implicitWidth: panels.resources.width
panel: panels.resourcesWrapper
radius: Appearance.rounding.large
x: panels.resourcesWrapper.x + panels.resources.x + root.borderThickness
y: panels.resourcesWrapper.y + panels.resources.y + bar.implicitHeight
}
onCleared: {
visibilities.launcher = false;
visibilities.sidebar = false;
visibilities.dashboard = false;
visibilities.osd = false;
visibilities.settings = false;
visibilities.resources = false;
visibilities.dock = false;
panels.popouts.hasCurrent = false;
}
PanelBg {
id: settingsBg
property real extraHeight: 0.2
deformAmount: 0.03
implicitHeight: panels.settings.height * (1 + extraHeight)
implicitWidth: panels.settings.width
panel: panels.settings
radius: Appearance.rounding.large
topLeftRadius: Appearance.rounding.large + Appearance.padding.smaller
topRightRadius: Appearance.rounding.large + Appearance.padding.smaller
x: panels.settingsWrapper.x + panels.settings.x + root.borderThickness
y: panels.settingsWrapper.y + panels.settings.y + bar.implicitHeight - panels.settings.height * extraHeight
}
PanelBg {
id: dockBg
deformAmount: 0.08
panel: panels.dock
radius: Appearance.rounding.normal
}
PanelBg {
id: drawingBg
deformAmount: 0.08
panel: panels.drawing
radius: Appearance.rounding.normal
}
PanelBg {
id: clipboardBg
deformAmount: 0.03
panel: panels.clipboard
radius: 29
}
}
Drawing {
id: drawing
anchors.fill: parent
layer.enabled: true
visibilities: visibilities
z: 2
layer.effect: MultiEffect {
maskEnabled: true
maskInverted: true
maskSource: maskSource
}
}
Item {
id: maskSource
anchors.fill: parent
layer.enabled: true
visible: false
CustomRect {
readonly property int extraWidth: radius
color: "white"
implicitHeight: panels.drawing.height
implicitWidth: panels.drawing.width + extraWidth
radius: drawingBg.radius
x: -extraWidth + root.borderThickness + panels.drawing.x
y: panels.drawing.y + bar.implicitHeight
}
}
Interactions {
id: interactions
anchors.fill: parent
bar: bar
borderThickness: root.borderLayoutThickness
drawing: drawing
enabled: true
panels: panels
popouts: panels.popouts
screen: root.screen
visibilities: visibilities
Panels {
id: panels
bar: bar
borderThickness: root.borderThickness
drawingItem: drawing
screen: root.screen
visibilities: visibilities
clipboard.transform: Matrix4x4 {
matrix: clipboardBg.deformMatrix
}
PersistentProperties {
id: visibilities
property bool bar
property bool dashboard
property bool dock
property bool isDrawing
property bool launcher
property bool notif: NotifServer.popups.length > 0
property bool osd
property bool resources
property bool settings
property bool sidebar
Component.onCompleted: Visibilities.load(scope.modelData, this)
dashboard.transform: Matrix4x4 {
matrix: dashBg.deformMatrix
}
IpcHandler {
function toggleLauncher(fix: string): void {
visibilities.launcher = !visibilities.launcher;
}
target: "visibilities"
dock.transform: Matrix4x4 {
matrix: dockBg.deformMatrix
}
Binding {
property: "bar"
target: visibilities
value: visibilities.sidebar || visibilities.dashboard || visibilities.osd || (!Config.barConfig.hideWhenNotif && visibilities.notif) || visibilities.resources || visibilities.settings || bar.isHovered
when: Config.barConfig.autoHide
launcher.transform: Matrix4x4 {
matrix: launcherBg.deformMatrix
}
Item {
anchors.fill: parent
layer.enabled: true
opacity: Appearance.transparency.enabled ? DynamicColors.transparency.base : 1
layer.effect: MultiEffect {
blurMax: 32
shadowColor: Qt.alpha(DynamicColors.palette.m3shadow, 1)
shadowEnabled: true
}
BlobGroup {
id: blobGroup
color: DynamicColors.palette.m3surface
smoothing: Config.barConfig.smoothing
Behavior on color {
CAnim {
}
}
}
BlobInvertedRect {
anchors.fill: parent
anchors.margins: -50
borderBottom: Config.barConfig.border - anchors.margins
borderLeft: Config.barConfig.border - anchors.margins
borderRight: Config.barConfig.border - anchors.margins
borderTop: bar.implicitHeight - anchors.margins
group: blobGroup
radius: Config.barConfig.rounding
}
PanelBg {
id: dashBg
property real extraHeight: 0.2
deformAmount: 0.06
implicitHeight: panels.dashboard.height * (1 + extraHeight)
implicitWidth: panels.dashboard.width
panel: panels.dashboardWrapper
radius: Appearance.rounding.normal
x: panels.dashboardWrapper.x + panels.dashboard.x + Config.barConfig.border
y: panels.dashboardWrapper.y + panels.dashboard.y + bar.implicitHeight - panels.dashboard.height * extraHeight
}
PanelBg {
id: launcherBg
property real extraHeight: 0.2
deformAmount: 0.06
implicitHeight: panels.launcher.height * (1 + extraHeight)
panel: panels.launcher
radius: Appearance.rounding.smallest + 5
y: panels.launcher.y + bar.implicitHeight
}
PanelBg {
id: sidebarBg
bottomLeftRadius: 0
deformAmount: 0.04
exclude: panels.sidebar.offsetScale > 0.08 ? [] : [utilsBg]
implicitHeight: panel.height * (1 / rawDeformMatrix.m22) + 2
panel: panels.sidebar
}
PanelBg {
id: osdBg
deformAmount: 0.1
implicitHeight: panels.osd.height
implicitWidth: panels.osd.width
panel: panels.osdWrapper
radius: 20
x: panels.osdWrapper.x + panels.osd.x + Config.barConfig.border
y: panels.osdWrapper.y + panels.osd.y + bar.implicitHeight
}
PanelBg {
id: notifsBg
panel: panels.notifications
radius: Appearance.rounding.normal
}
PanelBg {
id: utilsBg
deformAmount: panels.sidebar.visible ? (0.1) : (0.1)
exclude: panels.sidebar.offsetScale > 0.08 ? [] : [sidebarBg]
panel: panels.utilities
topLeftRadius: 0
}
PanelBg {
id: popoutBg
property real extraHeight: panels.popouts.isDetached ? 0 : 0.2
deformAmount: panels.popouts.isDetached ? 0.05 : panels.popouts.hasCurrent ? 0.15 : 0.1
implicitHeight: panels.popouts.height * (1 + extraHeight)
implicitWidth: panels.popouts.width
panel: panels.popoutsWrapper
radius: (panels.popouts.currentName.startsWith("audio") || panels.popouts.currentName.startsWith("updates")) ? Appearance.rounding.normal : 20 * Appearance.rounding.scale
x: panels.popoutsWrapper.x + panels.popouts.x + Config.barConfig.border
y: panels.popoutsWrapper.y + panels.popouts.y + bar.implicitHeight - panels.popouts.height * extraHeight
Behavior on extraHeight {
Anim {
}
}
}
PanelBg {
id: resourcesBg
deformAmount: 0.05
implicitHeight: panels.resources.height
implicitWidth: panels.resources.width
panel: panels.resourcesWrapper
radius: Appearance.rounding.normal
x: panels.resourcesWrapper.x + panels.resources.x + Config.barConfig.border
y: panels.resourcesWrapper.y + panels.resources.y + bar.implicitHeight
}
PanelBg {
id: settingsBg
property real extraHeight: 0.2
deformAmount: 0.03
implicitHeight: panels.settings.height * (1 + extraHeight)
implicitWidth: panels.settings.width
panel: panels.settings
radius: Appearance.rounding.large
topLeftRadius: Appearance.rounding.large + Appearance.padding.smaller
topRightRadius: Appearance.rounding.large + Appearance.padding.smaller
x: panels.settingsWrapper.x + panels.settings.x + Config.barConfig.border
y: panels.settingsWrapper.y + panels.settings.y + bar.implicitHeight - panels.settings.height * extraHeight
}
PanelBg {
id: dockBg
deformAmount: 0.08
panel: panels.dock
radius: Appearance.rounding.normal
}
PanelBg {
id: drawingBg
deformAmount: 0.08
panel: panels.drawing
radius: Appearance.rounding.normal
}
notifications.transform: Matrix4x4 {
matrix: notifsBg.deformMatrix
}
Loader {
id: drawingLoader
active: visibilities.isDrawing
anchors.fill: parent
z: 2
sourceComponent: Drawing {
id: drawing
}
osd.transform: Matrix4x4 {
matrix: osdBg.deformMatrix
}
Loader {
id: inputLoader
active: visibilities.isDrawing
anchors.fill: parent
z: 2
sourceComponent: DrawingInput {
id: input
bar: bar
drawing: drawingLoader.item
panels: panels
popout: panels.drawing
visibilities: visibilities
}
popouts.transform: Matrix4x4 {
matrix: popoutBg.deformMatrix
}
Interactions {
id: mouseArea
anchors.fill: parent
bar: bar
drawing: drawingLoader.item
enabled: true
input: inputLoader.item
panels: panels
popouts: panels.popouts
screen: scope.modelData
visibilities: visibilities
z: 1
Panels {
id: panels
bar: bar
drawingItem: drawingLoader.item
screen: scope.modelData
visibilities: visibilities
dashboard.transform: Matrix4x4 {
matrix: dashBg.deformMatrix
}
dock.transform: Matrix4x4 {
matrix: dockBg.deformMatrix
}
launcher.transform: Matrix4x4 {
matrix: launcherBg.deformMatrix
}
notifications.transform: Matrix4x4 {
matrix: notifsBg.deformMatrix
}
osd.transform: Matrix4x4 {
matrix: osdBg.deformMatrix
}
popouts.transform: Matrix4x4 {
matrix: popoutBg.deformMatrix
}
resources.transform: Matrix4x4 {
matrix: resourcesBg.deformMatrix
}
settings.transform: Matrix4x4 {
matrix: settingsBg.deformMatrix
}
sidebar.transform: Matrix4x4 {
matrix: sidebarBg.deformMatrix
}
utilities.transform: Matrix4x4 {
matrix: utilsBg.deformMatrix
}
}
BarLoader {
id: bar
anchors.left: parent.left
anchors.right: parent.right
popouts: panels.popouts
popoutsWrapper: panels.popoutsWrapper
screen: scope.modelData
visibilities: visibilities
}
resources.transform: Matrix4x4 {
matrix: resourcesBg.deformMatrix
}
settings.transform: Matrix4x4 {
matrix: settingsBg.deformMatrix
}
sidebar.transform: Matrix4x4 {
matrix: sidebarBg.deformMatrix
}
utilities.transform: Matrix4x4 {
matrix: utilsBg.deformMatrix
}
}
BarLoader {
id: bar
anchors.left: parent.left
anchors.right: parent.right
enabled: !visibilities.isDrawing
fullscreen: root.hasFullscreen
popouts: panels.popouts
popoutsWrapper: panels.popoutsWrapper
screen: root.screen
visibilities: visibilities
}
}
@@ -412,7 +475,7 @@ Variants {
implicitHeight: panel.height
implicitWidth: panel.width
radius: Appearance.rounding.smallest
x: panel.x + Config.barConfig.border
x: panel.x + root.borderThickness
y: panel.y + bar.implicitHeight
}
}
+7 -7
View File
@@ -118,12 +118,12 @@ ColumnLayout {
}
StateLayer {
function onClicked(): void {
parent.forceActiveFocus();
}
cursorShape: Qt.IBeamCursor
hoverEnabled: false
onClicked: {
parent.forceActiveFocus();
}
}
RowLayout {
@@ -179,11 +179,11 @@ ColumnLayout {
radius: Appearance.rounding.full
StateLayer {
function onClicked(): void {
color: root.greeter.buffer && !root.greeter.launching ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface
onClicked: {
root.greeter.submit();
}
color: root.greeter.buffer && !root.greeter.launching ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface
}
MaterialIcon {
+58 -3
View File
@@ -2,7 +2,62 @@ import QtQuick
import qs.Config
NumberAnimation {
duration: MaterialEasing.standardTime
easing.bezierCurve: MaterialEasing.standard
easing.type: Easing.BezierSpline
enum Type {
StandardSmall = 0,
Standard,
StandardLarge,
StandardExtraLarge,
EmphasizedSmall,
Emphasized,
EmphasizedLarge,
EmphasizedExtraLarge,
FastSpatial,
DefaultSpatial,
SlowSpatial,
FastEffects,
DefaultEffects,
SlowEffects
}
property int type: Anim.DefaultSpatial
duration: {
if (type < Anim.StandardSmall || type > Anim.SlowEffects)
return Appearance.anim.durations.normal;
if (type === Anim.FastSpatial)
return Appearance.anim.durations.expressiveFastSpatial;
if (type === Anim.DefaultSpatial)
return Appearance.anim.durations.expressiveDefaultSpatial;
if (type === Anim.SlowSpatial)
return Appearance.anim.durations.large;
if (type === Anim.FastEffects)
return Appearance.anim.durations.expressiveFastEffects;
if (type === Anim.DefaultEffects)
return Appearance.anim.durations.expressiveEffects;
if (type === Anim.SlowEffects)
return Appearance.anim.durations.expressiveSlowEffects;
const types = ["small", "normal", "large", "extraLarge"];
const idx = type % 4; // 0-7 are the 4 standard types
return Appearance.anim.durations[types[idx]];
}
easing.bezierCurve: {
if (type === Anim.FastSpatial)
return Appearance.anim.curves.expressiveFastSpatial;
if (type === Anim.DefaultSpatial)
return Appearance.anim.curves.expressiveDefaultSpatial;
if (type === Anim.SlowSpatial)
return Appearance.anim.curves.expressiveSlowSpatial;
if (type === Anim.FastEffects)
return Appearance.anim.curves.expressiveFastEffects;
if (type === Anim.DefaultEffects)
return Appearance.anim.curves.expressiveDefaultEffects;
if (type === Anim.SlowEffects)
return Appearance.anim.curves.expressiveSlowEffects;
if (type >= Anim.EmphasizedSmall && type <= Anim.EmphasizedExtraLarge)
return Appearance.anim.curves.emphasized;
return Appearance.anim.curves.standard;
}
}
+5 -5
View File
@@ -59,15 +59,15 @@ ColumnLayout {
}
StateLayer {
function onClicked(): void {
root.toggleRequested();
root.expanded = !root.expanded;
}
anchors.fill: parent
color: DynamicColors.palette.m3onSurface
radius: Appearance.rounding.normal
showHoverBackground: false
onClicked: {
root.toggleRequested();
root.expanded = !root.expanded;
}
}
}
+3 -3
View File
@@ -23,10 +23,10 @@ Button {
}
StateLayer {
function onClicked(): void {
radius: control.radius
onClicked: {
control.clicked();
}
radius: control.radius
}
}
+4 -4
View File
@@ -33,13 +33,13 @@ RadioButton {
}
StateLayer {
function onClicked(): void {
root.click();
}
anchors.margins: -7
color: root.checked ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3primary
z: -1
onClicked: {
root.click();
}
}
CustomRect {
+155 -26
View File
@@ -1,45 +1,174 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Templates
import ZShell.Components
import ZShell
import qs.Components
import qs.Config
Slider {
id: root
background: Item {
property bool animateWave
property color bgColor: enabled ? DynamicColors.palette.m3secondaryContainer : Qt.alpha(DynamicColors.palette.m3onSurface, 0.1)
property color fgColor: enabled ? DynamicColors.palette.m3primary : Qt.alpha(DynamicColors.palette.m3onSurface, 0.38)
property real filledWidth
property real pos: visualPosition
property int waveDuration: 1000
property real waveFrequency: 6
property bool wavy
signal interaction(v: real)
implicitHeight: 12
implicitWidth: 200
contentItem: Item {
anchors.fill: parent
CustomRect {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.top: parent.top
bottomRightRadius: root.implicitHeight / 6
color: DynamicColors.palette.m3primary
implicitWidth: root.handle.x - root.implicitHeight / 2
radius: Appearance.rounding.full
topRightRadius: root.implicitHeight / 6
id: remaining
anchors.left: handle.right
anchors.leftMargin: Appearance.spacing.extraSmall
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
bottomLeftRadius: Appearance.rounding.extraSmall / 2
color: root.bgColor
implicitHeight: parent.height * (parent.height <= 12 ? opacity : Math.min(opacity * 2, 1))
opacity: Math.min(width, 12) / 12
radius: Appearance.rounding.small
topLeftRadius: Appearance.rounding.extraSmall / 2
}
CustomRect {
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.top: parent.top
bottomLeftRadius: root.implicitHeight / 6
color: DynamicColors.tPalette.m3surfaceContainer
implicitWidth: parent.width - root.handle.x - root.handle.implicitWidth - root.implicitHeight / 2
anchors.rightMargin: 4 * remaining.opacity
anchors.verticalCenter: parent.verticalCenter
color: root.fgColor
implicitHeight: 4 * remaining.opacity
implicitWidth: implicitHeight
opacity: remaining.opacity
radius: Appearance.rounding.full
topLeftRadius: root.implicitHeight / 6
}
CustomRect {
id: handle
anchors.left: filled.right
anchors.leftMargin: Appearance.spacing.extraSmall
anchors.verticalCenter: parent.verticalCenter
color: root.fgColor
implicitHeight: {
const mult = parent.height <= 12 ? 3 : 1.2;
const pressMult = parent.height <= 12 ? 4 : 1.5;
return parent.height * (mouse.pressed ? pressMult : mult);
}
implicitWidth: 4
radius: Appearance.rounding.full
Behavior on implicitHeight {
Anim {
type: Anim.FastSpatial
}
}
}
Loader {
id: filled
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
asynchronous: true
sourceComponent: root.wavy ? waveComp : lineComp
}
Component {
id: lineComp
CustomRect {
bottomRightRadius: Appearance.rounding.extraSmall / 2
color: root.fgColor
implicitHeight: root.height
implicitWidth: root.filledWidth
radius: Appearance.rounding.small
topRightRadius: Appearance.rounding.extraSmall / 2
}
}
Component {
id: waveComp
WavyLine {
color: root.fgColor
frequency: root.waveFrequency
fullLength: root.width - handle.implicitWidth - handle.anchors.leftMargin
implicitHeight: lineWidth * amplitudeMultiplier * 2 + lineWidth
implicitWidth: root.filledWidth
lineWidth: root.height * 0.7
startX: x
Behavior on color {
CAnim {
}
}
Anim on waveProgress {
duration: root.waveDuration
easing.type: Easing.Linear
from: 0
loops: Animation.Infinite
paused: !root.animateWave
running: true
to: 1
}
}
}
}
handle: CustomRect {
anchors.verticalCenter: parent.verticalCenter
color: DynamicColors.palette.m3primary
implicitHeight: 15
implicitWidth: 5
radius: Appearance.rounding.full
x: root.visualPosition * root.availableWidth - implicitWidth / 2
Behavior on filledWidth {
id: widthBehavior
MouseArea {
acceptedButtons: Qt.NoButton
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
Anim {
}
}
Component.onCompleted: filledWidth = Qt.binding(() => (width - handle.implicitWidth - handle.anchors.leftMargin) * pos)
Binding {
id: posBinding
property: "pos"
target: root
value: ZUtils.clamp(mouse.pressStartPos + mouse.dragMovement, 0, 1)
when: mouse.pressed
}
MouseArea {
id: mouse
property real dragMovement
property real pressStartPos
property real pressStartX
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
implicitHeight: handle.implicitHeight
preventStealing: true
onPositionChanged: e => {
dragMovement = (e.x - pressStartX) / width;
root.interaction(posBinding.value);
}
onPressed: e => {
widthBehavior.enabled = false;
pressStartX = e.x;
pressStartPos = root.visualPosition;
}
onReleased: e => {
root.interaction(posBinding.value);
widthBehavior.enabled = true;
dragMovement = 0;
}
}
}
+12 -13
View File
@@ -28,6 +28,7 @@ RowLayout {
CustomTextField {
id: textField
color: root.enabled ? DynamicColors.palette.m3onSurface : Qt.alpha(DynamicColors.palette.m3onSurface, 0.5)
implicitHeight: upButton.implicitHeight
inputMethodHints: Qt.ImhFormattedNumbersOnly
leftPadding: Appearance.padding.normal
@@ -36,7 +37,7 @@ RowLayout {
text: root.isEditing ? text : root.displayText
background: CustomRect {
color: DynamicColors.tPalette.m3surfaceContainerHigh
color: root.enabled ? DynamicColors.tPalette.m3surfaceContainerHigh : DynamicColors.tPalette.m3surfaceContainerLow
implicitWidth: 100
radius: Appearance.rounding.full
}
@@ -85,7 +86,7 @@ RowLayout {
CustomRect {
id: upButton
color: DynamicColors.palette.m3primary
color: root.enabled ? DynamicColors.palette.m3primary : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 1)
implicitHeight: upIcon.implicitHeight + Appearance.padding.small * 2
implicitWidth: implicitHeight
radius: Appearance.rounding.full
@@ -93,7 +94,9 @@ RowLayout {
StateLayer {
id: upState
function onClicked(): void {
color: DynamicColors.palette.m3onPrimary
onClicked: {
let newValue = Math.min(root.max, root.value + root.step);
// Round to avoid floating point precision errors
const decimals = root.step < 1 ? Math.max(1, Math.ceil(-Math.log10(root.step))) : 0;
@@ -102,9 +105,6 @@ RowLayout {
root.displayText = newValue.toString();
root.valueModified(newValue);
}
color: DynamicColors.palette.m3onPrimary
onPressAndHold: timer.start()
onReleased: timer.stop()
}
@@ -113,13 +113,13 @@ RowLayout {
id: upIcon
anchors.centerIn: parent
color: DynamicColors.palette.m3onPrimary
color: root.enabled ? DynamicColors.palette.m3onPrimary : Qt.alpha(DynamicColors.palette.m3onSurface, 0.5)
text: "keyboard_arrow_up"
}
}
CustomRect {
color: DynamicColors.palette.m3primary
color: root.enabled ? DynamicColors.palette.m3primary : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 1)
implicitHeight: downIcon.implicitHeight + Appearance.padding.small * 2
implicitWidth: implicitHeight
radius: Appearance.rounding.full
@@ -127,7 +127,9 @@ RowLayout {
StateLayer {
id: downState
function onClicked(): void {
color: DynamicColors.palette.m3onPrimary
onClicked: {
let newValue = Math.max(root.min, root.value - root.step);
// Round to avoid floating point precision errors
const decimals = root.step < 1 ? Math.max(1, Math.ceil(-Math.log10(root.step))) : 0;
@@ -136,9 +138,6 @@ RowLayout {
root.displayText = newValue.toString();
root.valueModified(newValue);
}
color: DynamicColors.palette.m3onPrimary
onPressAndHold: timer.start()
onReleased: timer.stop()
}
@@ -147,7 +146,7 @@ RowLayout {
id: downIcon
anchors.centerIn: parent
color: DynamicColors.palette.m3onPrimary
color: root.enabled ? DynamicColors.palette.m3onPrimary : Qt.alpha(DynamicColors.palette.m3onSurface, 0.5)
text: "keyboard_arrow_down"
}
}
+36 -69
View File
@@ -1,7 +1,6 @@
import QtQuick
import QtQuick.Layouts
import qs.Config
import qs.Helpers
Row {
id: root
@@ -12,66 +11,46 @@ Row {
}
property alias active: menu.active
property color color: type == CustomSplitButton.Filled ? DynamicColors.palette.m3primary : DynamicColors.palette.m3secondaryContainer
property color colour: type == CustomSplitButton.Filled ? DynamicColors.palette.m3primary : DynamicColors.palette.m3secondaryContainer
property bool disabled
property color disabledColor: Qt.alpha(DynamicColors.palette.m3onSurface, 0.1)
property color disabledTextColor: Qt.alpha(DynamicColors.palette.m3onSurface, 0.38)
property color disabledColour: Qt.alpha(DynamicColors.palette.m3onSurface, 0.1)
property color disabledTextColour: Qt.alpha(DynamicColors.palette.m3onSurface, 0.38)
readonly property alias expandBtn: expandBtn
property alias expanded: menu.expanded
property string fallbackIcon
property string fallbackText
property real horizontalPadding: Appearance.padding.normal
property alias iconLabel: iconLabel
property alias label: label
property alias menu: menu
property real horizontalPadding: Appearance.padding.larger
readonly property alias iconLabel: iconLabel
readonly property alias label: label
readonly property alias menu: menu
property alias menuItems: menu.items
property bool menuOnTop
property alias stateLayer: stateLayer
property color textColor: type == CustomSplitButton.Filled ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSecondaryContainer
property real minLeftWidth
readonly property alias stateLayer: stateLayer
property color textColour: type == CustomSplitButton.Filled ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSecondaryContainer
readonly property alias textRow: textRow
property int type: CustomSplitButton.Filled
property real verticalPadding: Appearance.padding.smaller
property real verticalPadding: Appearance.padding.small
function closeDropdown(): void {
SettingsDropdowns.close(menu);
}
function openDropdown(): void {
if (root.disabled)
return;
SettingsDropdowns.open(menu, root);
}
function toggleDropdown(): void {
if (root.disabled)
return;
SettingsDropdowns.toggle(menu, root);
}
spacing: Math.floor(Appearance.spacing.small / 2)
onExpandedChanged: {
if (!expanded)
SettingsDropdowns.forget(menu);
}
spacing: Math.floor(Appearance.spacing.extraSmall)
CustomRect {
bottomRightRadius: Appearance.rounding.small / 2
color: root.disabled ? root.disabledColor : root.color
color: root.disabled ? root.disabledColour : root.colour
implicitHeight: expandBtn.implicitHeight
implicitWidth: textRow.implicitWidth + root.horizontalPadding * 2
implicitWidth: Math.max(root.minLeftWidth, textRow.implicitWidth + root.horizontalPadding * 2)
radius: implicitHeight / 2 * Math.min(1, Appearance.rounding.scale)
topRightRadius: Appearance.rounding.small / 2
StateLayer {
id: stateLayer
function onClicked(): void {
root.active?.clicked();
}
color: root.textColor
bottomRightRadius: parent.bottomRightRadius
color: root.textColour
disabled: root.disabled
rect.bottomRightRadius: parent.bottomRightRadius
rect.topRightRadius: parent.topRightRadius
topRightRadius: parent.topRightRadius
onClicked: root.active?.clicked()
}
RowLayout {
@@ -86,7 +65,7 @@ Row {
Layout.alignment: Qt.AlignVCenter
animate: true
color: root.disabled ? root.disabledTextColor : root.textColor
color: root.disabled ? root.disabledTextColour : root.textColour
fill: 1
text: root.active?.activeIcon ?? root.fallbackIcon
}
@@ -98,12 +77,12 @@ Row {
Layout.preferredWidth: implicitWidth
animate: true
clip: true
color: root.disabled ? root.disabledTextColor : root.textColor
color: root.disabled ? root.disabledTextColour : root.textColour
text: root.active?.activeText ?? root.fallbackText
Behavior on Layout.preferredWidth {
Anim {
easing.bezierCurve: Appearance.anim.curves.emphasized
type: Anim.Emphasized
}
}
}
@@ -116,7 +95,7 @@ Row {
property real rad: root.expanded ? implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) : Appearance.rounding.small / 2
bottomLeftRadius: rad
color: root.disabled ? root.disabledColor : root.color
color: root.disabled ? root.disabledColour : root.colour
implicitHeight: expandIcon.implicitHeight + root.verticalPadding * 2
implicitWidth: implicitHeight
radius: implicitHeight / 2 * Math.min(1, Appearance.rounding.scale)
@@ -130,14 +109,12 @@ Row {
StateLayer {
id: expandStateLayer
function onClicked(): void {
root.toggleDropdown();
}
color: root.textColor
color: root.textColour
disabled: root.disabled
rect.bottomLeftRadius: parent.bottomLeftRadius
rect.topLeftRadius: parent.topLeftRadius
onClicked: root.expanded = !root.expanded
}
MaterialIcon {
@@ -145,7 +122,7 @@ Row {
anchors.centerIn: parent
anchors.horizontalCenterOffset: root.expanded ? 0 : -Math.floor(root.verticalPadding / 4)
color: root.disabled ? root.disabledTextColor : root.textColor
color: root.disabled ? root.disabledTextColour : root.textColour
rotation: root.expanded ? 180 : 0
text: "expand_more"
@@ -158,24 +135,14 @@ Row {
}
}
}
}
Menu {
id: menu
Menu {
id: menu
anchors.bottomMargin: Appearance.spacing.small
anchors.right: parent.right
anchors.top: parent.bottom
anchors.topMargin: Appearance.spacing.small
states: State {
when: root.menuOnTop
AnchorChanges {
anchors.bottom: expandBtn.top
anchors.top: undefined
target: menu
}
}
}
attachSideY: root.menuOnTop ? Menu.Top : Menu.Bottom
attachTo: expandBtn
marginY: Appearance.spacing.small * (root.menuOnTop ? -1 : 1)
thisSideY: root.menuOnTop ? Menu.Bottom : Menu.Top
}
}
+19 -8
View File
@@ -8,19 +8,32 @@ Item {
id: root
property alias active: splitButton.active
property bool enabled: true
property alias buttonAlias: splitButton
property alias expanded: splitButton.expanded
property int expandedZ: 100
required property string label
property alias menuItems: splitButton.menuItems
property bool shouldBeActive: true
property alias type: splitButton.type
signal selected(item: MenuItem)
Layout.fillWidth: true
Layout.preferredHeight: row.implicitHeight + Appearance.padding.smaller * 2
anchors.left: parent.left
anchors.right: parent.right
clip: false
z: root.expanded ? expandedZ : -1
implicitHeight: row.implicitHeight + Appearance.padding.smaller * 2
opacity: shouldBeActive ? 1 : 0
scale: shouldBeActive ? 1 : 0.8
z: splitButton.menu.implicitHeight > 0 ? expandedZ : 1
Behavior on opacity {
Anim {
}
}
Behavior on scale {
Anim {
}
}
RowLayout {
id: row
@@ -36,7 +49,6 @@ Item {
color: root.enabled ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.larger
text: root.label
z: root.expanded ? root.expandedZ : -1
}
CustomSplitButton {
@@ -44,14 +56,13 @@ Item {
enabled: root.enabled
type: CustomSplitButton.Filled
z: root.expanded ? root.expandedZ : -1
z: 2
menu.onItemSelected: item => {
root.selected(item);
splitButton.closeDropdown();
}
stateLayer.onClicked: {
splitButton.toggleDropdown();
splitButton.expanded = !splitButton.expanded;
}
}
}
+18 -16
View File
@@ -1,6 +1,6 @@
import QtQuick
import QtQuick.Templates
import QtQuick.Shapes
import QtQuick.Templates
import qs.Config
Switch {
@@ -12,38 +12,41 @@ Switch {
implicitWidth: implicitIndicatorWidth
indicator: CustomRect {
color: root.checked ? DynamicColors.palette.m3primary : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, root.cLayer)
implicitHeight: 13 + 7 * 2
color: root.checked && root.enabled ? DynamicColors.palette.m3primary : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, root.cLayer)
implicitHeight: Appearance.font.size.medium + Appearance.padding.normal * 2
implicitWidth: implicitHeight * 1.7
radius: Appearance.rounding.full
CustomRect {
readonly property real nonAnimWidth: root.pressed ? implicitHeight * 1.3 : implicitHeight
readonly property real nonAnimWidth: root.pressed ? implicitHeight * 1.2 : implicitHeight
anchors.verticalCenter: parent.verticalCenter
color: root.checked ? DynamicColors.palette.m3onPrimary : DynamicColors.layer(DynamicColors.palette.m3outline, root.cLayer + 1)
implicitHeight: parent.implicitHeight - 10
color: root.checked && root.enabled ? DynamicColors.palette.m3onPrimary : DynamicColors.layer(DynamicColors.palette.m3outline, root.cLayer + 1)
implicitHeight: parent.implicitHeight - Appearance.padding.extraSmall
implicitWidth: nonAnimWidth
radius: Appearance.rounding.full
x: root.checked ? parent.implicitWidth - nonAnimWidth - 10 / 2 : 10 / 2
x: root.checked ? parent.implicitWidth - nonAnimWidth - Appearance.padding.extraSmall / 2 : Appearance.padding.extraSmall / 2
Behavior on implicitWidth {
Anim {
type: Anim.FastSpatial
}
}
Behavior on x {
Anim {
type: Anim.FastSpatial
}
}
CustomRect {
anchors.fill: parent
color: root.checked ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurface
color: root.checked && root.enabled ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurface
opacity: root.pressed ? 0.1 : root.hovered ? 0.08 : 0
radius: parent.radius
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
}
@@ -63,14 +66,14 @@ Switch {
}
property point end2: {
if (root.pressed)
return Qt.point(width, height / 2);
return Qt.point(width * 0.8, height / 2);
if (root.checked)
return Qt.point(width * 0.85, height * 0.2);
return Qt.point(width * 0.85, height * 0.15);
}
property point start1: {
if (root.pressed)
return Qt.point(width * 0.1, height / 2);
return Qt.point(width * 0.2, height / 2);
if (root.checked)
return Qt.point(width * 0.15, height / 2);
return Qt.point(width * 0.15, height * 0.15);
@@ -88,7 +91,7 @@ Switch {
anchors.centerIn: parent
asynchronous: true
height: parent.implicitHeight - Appearance.padding.small * 2
height: parent.implicitHeight - Appearance.padding.larger
preferredRendererType: Shape.CurveRenderer
width: height
@@ -110,11 +113,11 @@ Switch {
}
ShapePath {
capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap
capStyle: ShapePath.RoundCap
fillColor: "transparent"
startX: icon.start1.x
startY: icon.start1.y
strokeColor: root.checked ? DynamicColors.palette.m3primary : DynamicColors.palette.m3surfaceContainerHighest
strokeColor: root.checked && root.enabled ? DynamicColors.palette.m3primary : DynamicColors.palette.m3surfaceContainerHighest
strokeWidth: Appearance.font.size.larger * 0.15
Behavior on strokeColor {
@@ -148,8 +151,7 @@ Switch {
}
component PropAnim: PropertyAnimation {
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
easing.type: Easing.BezierSpline
duration: Appearance.anim.durations.expressiveFastSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
}
}
+1
View File
@@ -15,6 +15,7 @@ Text {
color: DynamicColors.palette.m3onSurface
font.family: Appearance.font.family.sans
font.pointSize: Appearance.font.size.normal
linkColor: DynamicColors.palette.m3onPrimaryFixedVariant
renderType: Text.NativeRendering
textFormat: Text.PlainText
+2 -1
View File
@@ -1,6 +1,6 @@
import qs.Config
import QtQuick
import QtQuick.Effects
import qs.Config
RectangularShadow {
property real dp: [0, 1, 3, 6, 8, 12][level]
@@ -13,6 +13,7 @@ RectangularShadow {
Behavior on dp {
Anim {
type: Anim.SlowEffects
}
}
}
+33
View File
@@ -0,0 +1,33 @@
import QtQuick
import QtQuick.Controls
import qs.Config
IconButton {
id: root
required property bool shouldBeVisible
opacity: 0
scale: 0
visible: root.scale > 0
Behavior on opacity {
Anim {
duration: Appearance.anim.durations.small
}
}
Behavior on scale {
Anim {
}
}
onShouldBeVisibleChanged: {
if (root.shouldBeVisible) {
root.opacity = 1;
root.scale = 1;
} else {
root.opacity = 0;
root.scale = 0;
}
}
}
+5 -6
View File
@@ -41,12 +41,11 @@ CustomRect {
color: type === IconButton.Text ? "transparent" : disabled ? disabledColour : internalChecked ? activeColour : inactiveColour
implicitHeight: label.implicitHeight + padding * 2
implicitWidth: implicitHeight
radius: internalChecked ? 6 : implicitHeight / 2 * Math.min(1, 1)
radius: internalChecked ? 6 : (implicitHeight / 2 * Math.min(1, 1)) * Appearance.rounding.scale
Behavior on radius {
Anim {
id: radiusAnim
}
}
@@ -55,14 +54,14 @@ CustomRect {
StateLayer {
id: stateLayer
function onClicked(): void {
color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour
disabled: root.disabled
onClicked: {
if (root.toggle)
root.internalChecked = !root.internalChecked;
root.clicked();
}
color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour
disabled: root.disabled
}
MaterialIcon {
+2
View File
@@ -102,6 +102,7 @@ Item {
animate: root.animate
animateProp: "opacity"
color: root.color
font.pointSize: elideText.font.pointSize
text: elideText.text
}
@@ -111,6 +112,7 @@ Item {
animate: root.animate
animateProp: "opacity"
color: root.color
font.pointSize: elideText.font.pointSize
text: t1.text
x: t1.width + root.gap
}
+129 -69
View File
@@ -2,109 +2,169 @@ pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Config
import qs.Drawers
Elevation {
MouseArea {
id: root
enum Side {
Top,
Bottom,
Left,
Right
}
property MenuItem active: items[0] ?? null
property int attachSideX: Menu.Right
property int attachSideY: Menu.Bottom
required property Item attachTo
property bool expanded
property list<MenuItem> items
property real marginX
property real marginY
property int thisSideX: Menu.Right
property int thisSideY: Menu.Top
signal itemSelected(item: MenuItem)
implicitHeight: root.expanded ? column.implicitHeight + Appearance.padding.small * 2 : 0
implicitWidth: Math.max(200, column.implicitWidth)
level: 2
opacity: root.expanded ? 1 : 0
radius: Appearance.rounding.normal
Behavior on implicitHeight {
Anim {
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
anchors.fill: parent
enabled: expanded
layer.enabled: opacity < 1
opacity: expanded ? 1 : 0
parent: {
const win = QsWindow.window;
const contentWin = win as Windows;
return contentWin ? contentWin.interactionWrapper : (win as QsWindow).contentItem;
}
Behavior on opacity {
Anim {
duration: Appearance.anim.durations.expressiveDefaultSpatial
type: Anim.DefaultEffects
}
}
CustomClippingRect {
anchors.fill: parent
color: DynamicColors.palette.m3surfaceContainer
radius: parent.radius
onClicked: expanded = false
ColumnLayout {
id: column
TransformWatcher {
id: watcher
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 5
a: root.parent
b: root.attachTo
}
Repeater {
model: root.items
Elevation {
id: menu
CustomRect {
id: item
implicitHeight: column.implicitHeight + column.anchors.margins * 2
implicitWidth: Math.max(200, column.implicitWidth + column.anchors.margins * 2)
level: 2
radius: Appearance.rounding.medium
x: {
watcher.transform;
const item = root.attachTo;
let off = root.attachSideX === Menu.Left ? 0 : item.width;
if (root.thisSideX === Menu.Right)
off -= width;
return item.mapToItem(root.parent, off, 0).x + root.marginX;
}
y: {
watcher.transform;
const item = root.attachTo;
let off = root.attachSideY === Menu.Top ? 0 : item.height;
if (root.thisSideY === Menu.Bottom)
off -= height;
return item.mapToItem(root.parent, 0, off).y + root.marginY;
}
readonly property bool active: modelData === root.active
required property int index
required property MenuItem modelData
transform: Scale {
origin.y: root.thisSideY === Menu.Bottom ? menu.height : 0
yScale: root.expanded ? 1 : 0.1
Layout.fillWidth: true
implicitHeight: menuOptionRow.implicitHeight + Appearance.padding.normal * 2
implicitWidth: menuOptionRow.implicitWidth + Appearance.padding.normal * 2
Behavior on yScale {
Anim {
}
}
}
CustomRect {
anchors.fill: parent
color: DynamicColors.palette.m3surfaceContainerLow
radius: parent.radius
ColumnLayout {
id: column
anchors.fill: parent
anchors.margins: Appearance.padding.extraSmall
spacing: Appearance.spacing.extraSmall
Repeater {
id: repeater
model: root.items
CustomRect {
anchors.fill: parent
anchors.leftMargin: Appearance.padding.small
anchors.rightMargin: Appearance.padding.small
color: Qt.alpha(DynamicColors.palette.m3secondaryContainer, active ? 1 : 0)
radius: Appearance.rounding.normal - Appearance.padding.small
id: item
readonly property bool active: modelData === root.active
required property int index
required property MenuItem modelData
Layout.fillWidth: true
color: Qt.alpha(DynamicColors.palette.m3tertiaryContainer, active ? 1 : 0)
implicitHeight: menuOptionRow.implicitHeight + Appearance.padding.larger * 2
implicitWidth: menuOptionRow.implicitWidth + Appearance.padding.larger * 2
radius: Appearance.rounding.small
Behavior on radius {
Anim {
}
}
StateLayer {
function onClicked(): void {
color: item.active ? DynamicColors.palette.m3onTertiaryContainer : DynamicColors.palette.m3onSurface
disabled: !root.expanded
onClicked: {
root.itemSelected(item.modelData);
root.active = item.modelData;
item.modelData.clicked();
root.expanded = false;
}
color: item.active ? DynamicColors.palette.m3onSecondaryContainer : DynamicColors.palette.m3onSurface
disabled: !root.expanded
}
}
RowLayout {
id: menuOptionRow
anchors.fill: parent
anchors.margins: Appearance.padding.normal
spacing: Appearance.spacing.small
MaterialIcon {
Layout.alignment: Qt.AlignVCenter
color: item.active ? DynamicColors.palette.m3onSecondaryContainer : DynamicColors.palette.m3onSurfaceVariant
text: item.modelData.icon
}
CustomText {
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
color: item.active ? DynamicColors.palette.m3onSecondaryContainer : DynamicColors.palette.m3onSurface
text: item.modelData.text
}
RowLayout {
id: menuOptionRow
Loader {
Layout.alignment: Qt.AlignVCenter
active: item.modelData.trailingIcon.length > 0
visible: active
anchors.fill: parent
anchors.margins: Appearance.padding.larger
spacing: Appearance.spacing.small
sourceComponent: MaterialIcon {
color: item.active ? DynamicColors.palette.m3onSecondaryContainer : DynamicColors.palette.m3onSurface
text: item.modelData.trailingIcon
MaterialIcon {
Layout.alignment: Qt.AlignVCenter
color: item.active ? DynamicColors.palette.m3onTertiaryContainer : DynamicColors.palette.m3onSurfaceVariant
text: item.modelData.icon
}
CustomText {
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
color: item.active ? DynamicColors.palette.m3onTertiaryContainer : DynamicColors.palette.m3onSurface
text: item.modelData.text
}
Loader {
Layout.alignment: Qt.AlignVCenter
active: item.modelData.trailingIcon.length > 0
asynchronous: true
visible: active
sourceComponent: MaterialIcon {
color: item.active ? DynamicColors.palette.m3onTertiaryContainer : DynamicColors.palette.m3onSurfaceVariant
text: item.modelData.trailingIcon
}
}
}
}
+2
View File
@@ -29,6 +29,7 @@ Elevation {
level: root.expanded ? 2 : 0
radius: itemHeight / 2
visible: implicitHeight > 0
z: root.expanded ? 100 : 0
Behavior on implicitHeight {
Anim {
@@ -68,6 +69,7 @@ Elevation {
anchors.fill: parent
color: DynamicColors.palette.m3surfaceContainer
radius: parent.radius
z: root.z
// Main visible spinner: normal/outside text color
PathView {
+172 -67
View File
@@ -1,15 +1,53 @@
import qs.Config
import QtQuick
import QtQuick.Shapes
import ZShell
import ZShell.Components
import qs.Helpers
import qs.Config
MouseArea {
id: root
property color color: DynamicColors.palette.m3onSurface
property alias bottomLeftRadius: base.bottomLeftRadius
property alias bottomRightRadius: base.bottomRightRadius
property real circleRadius
property alias color: base.color
property bool disabled
property real radius: parent?.radius ?? 0
property alias rect: hoverLayer
readonly property real endRadius: {
const d1 = distSq(0, 0);
const d2 = distSq(width, 0);
const d3 = distSq(0, height);
const d4 = distSq(width, height);
return (Math.sqrt(Math.max(d1, d2, d3, d4)) + (shapeMorph ? 24 : 0)) * 1.3;
}
property real endRadiusAtPress
property bool manualPressOverride
property real pressX: width / 2
property real pressY: height / 2
property alias radius: base.radius
readonly property alias rect: base
property bool shapeMorph
property bool showHoverBackground: true
property real stateOpacity: containsMouse ? 0.08 : 0
property alias topLeftRadius: base.topLeftRadius
property alias topRightRadius: base.topRightRadius
function onClicked(): void {
function clamp(r: real): real {
return Math.max(0, Math.min(r, width / 2, height / 2));
}
function distSq(x: real, y: real): real {
return (pressX - x) ** 2 + (pressY - y) ** 2;
}
function press(x: real, y: real): void {
pressX = x;
pressY = y;
fadeAnim.complete();
circleRadius = 0;
circle.opacity = 0.1;
rippleAnim.restart();
endRadiusAtPress = endRadius;
}
anchors.fill: parent
@@ -17,79 +55,146 @@ MouseArea {
enabled: !disabled
hoverEnabled: true
onClicked: event => !disabled && onClicked(event)
onPressed: event => {
if (disabled)
return;
rippleAnim.x = event.x;
rippleAnim.y = event.y;
const dist = (ox, oy) => ox * ox + oy * oy;
rippleAnim.radius = Math.sqrt(Math.max(dist(event.x, event.y), dist(event.x, height - event.y), dist(width - event.x, event.y), dist(width - event.x, height - event.y)));
rippleAnim.restart();
Behavior on stateOpacity {
Anim {
type: Anim.DefaultEffects
}
}
SequentialAnimation {
onCircleRadiusChanged: {
if (!(pressed || manualPressOverride) && circleRadius > endRadiusAtPress * 0.99 && !fadeAnim.running)
fadeAnim.start();
}
onClicked: event => !disabled && onClicked(event)
onManualPressOverrideChanged: {
if (!(pressed || manualPressOverride) && circleRadius > endRadiusAtPress * 0.99 && !fadeAnim.running)
fadeAnim.start();
}
onPressed: e => press(e.x, e.y)
onPressedChanged: {
if (!(pressed || manualPressOverride) && !rippleAnim.running && circle.opacity > 0)
fadeAnim.start();
}
Anim {
id: rippleAnim
property real radius
property real x
property real y
PropertyAction {
property: "x"
target: ripple
value: rippleAnim.x
}
PropertyAction {
property: "y"
target: ripple
value: rippleAnim.y
}
PropertyAction {
property: "opacity"
target: ripple
value: 0.08
}
Anim {
easing.bezierCurve: MaterialEasing.standardDecel
from: 0
properties: "implicitWidth,implicitHeight"
target: ripple
to: rippleAnim.radius * 2
}
Anim {
property: "opacity"
target: ripple
to: 0
}
alwaysRunToEnd: true
duration: Appearance.anim.durations.expressiveSlowEffects * 2
easing.bezierCurve: Appearance.anim.curves.standard
property: "circleRadius"
target: root
to: root.endRadius
}
CustomClippingRect {
id: hoverLayer
Anim {
id: fadeAnim
property: "opacity"
target: circle
to: 0
type: Anim.SlowEffects
}
CustomRect {
id: base
anchors.fill: parent
border.pixelAligned: false
color: Qt.alpha(root.color, root.disabled ? 0 : root.pressed ? 0.1 : root.containsMouse ? 0.08 : 0)
radius: root.radius
bottomLeftRadius: root.parent?.bottomLeftRadius ?? radius ?? 0
bottomRightRadius: root.parent?.bottomRightRadius ?? radius ?? 0
color: DynamicColors.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
topLeftRadius: root.parent?.topLeftRadius ?? radius ?? 0
topRightRadius: root.parent?.topRightRadius ?? radius ?? 0
// qmllint enable missing-property
}
CustomRect {
id: ripple
Shape {
id: circle
border.pixelAligned: false
color: root.color
opacity: 0
radius: Appearance.rounding.full
anchors.fill: parent
opacity: 0
preferredRendererType: Shape.CurveRenderer
transform: Translate {
x: -ripple.width / 2
y: -ripple.height / 2
ShapePath {
fillColor: base.color
startX: root.clamp(base.topLeftRadius)
startY: 0
strokeColor: "transparent"
strokeWidth: 0
fillGradient: RadialGradient {
centerRadius: root.circleRadius
centerX: root.pressX
centerY: root.pressY
focalX: centerX
focalY: centerY
GradientStop {
color: Qt.alpha(base.color, 1)
position: 0
}
GradientStop {
color: Qt.alpha(base.color, 1)
position: ZUtils.clamp(1 - 0.2 * root.endRadius / root.circleRadius, 0.01, 0.99)
}
GradientStop {
color: Qt.alpha(base.color, ZUtils.clamp((root.circleRadius / root.endRadius - 0.9) / 0.1, 0, 1))
position: 1
}
}
PathLine {
x: root.width - root.clamp(base.topRightRadius)
y: 0
}
PathArc {
radiusX: root.clamp(base.topRightRadius)
radiusY: root.clamp(base.topRightRadius)
relativeX: root.clamp(base.topRightRadius)
relativeY: root.clamp(base.topRightRadius)
}
PathLine {
x: root.width
y: root.height - root.clamp(base.bottomRightRadius)
}
PathArc {
radiusX: root.clamp(base.bottomRightRadius)
radiusY: root.clamp(base.bottomRightRadius)
relativeX: -root.clamp(base.bottomRightRadius)
relativeY: root.clamp(base.bottomRightRadius)
}
PathLine {
x: root.clamp(base.bottomLeftRadius)
y: root.height
}
PathArc {
radiusX: root.clamp(base.bottomLeftRadius)
radiusY: root.clamp(base.bottomLeftRadius)
relativeX: -root.clamp(base.bottomLeftRadius)
relativeY: -root.clamp(base.bottomLeftRadius)
}
PathLine {
x: 0
y: root.clamp(base.topLeftRadius)
}
PathArc {
radiusX: root.clamp(base.topLeftRadius)
radiusY: root.clamp(base.topLeftRadius)
x: root.clamp(base.topLeftRadius)
y: 0
}
}
}
+1 -2
View File
@@ -4,10 +4,9 @@ import Quickshell
Singleton {
readonly property AppearanceConf.Anim anim: Config.appearance.anim
readonly property AppearanceConf.Deform deform: Config.appearance.deform
readonly property AppearanceConf.FontStuff font: Config.appearance.font
readonly property AppearanceConf.Padding padding: Config.appearance.padding
// Literally just here to shorten accessing stuff :woe:
// Also kinda so I can keep accessing it with `Appearance.xxx` instead of `Conf.appearance.xxx`
readonly property AppearanceConf.Rounding rounding: Config.appearance.rounding
readonly property AppearanceConf.Spacing spacing: Config.appearance.spacing
readonly property AppearanceConf.Transparency transparency: Config.appearance.transparency
+24 -7
View File
@@ -3,6 +3,8 @@ import Quickshell.Io
JsonObject {
property Anim anim: Anim {
}
property Deform deform: Deform {
}
property FontStuff font: FontStuff {
}
property Padding padding: Padding {
@@ -26,9 +28,13 @@ JsonObject {
property list<real> emphasized: [0.05, 0, 2 / 15, 0.06, 1 / 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1]
property list<real> emphasizedAccel: [0.3, 0, 0.8, 0.15, 1, 1]
property list<real> emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1]
property list<real> expressiveDefaultEffects: [0.34, 0.8, 0.34, 1, 1, 1]
property list<real> expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1]
property list<real> expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1]
property list<real> expressiveFastEffects: [0.31, 0.94, 0.34, 1, 1, 1]
property list<real> expressiveFastSpatial: [0.42, 1.67, 0.21, 0.9, 1, 1]
property list<real> expressiveSlowEffects: [0.34, 0.88, 0.34, 1, 1, 1]
property list<real> expressiveSlowSpatial: [0.39, 1.29, 0.35, 0.98, 1, 1]
property list<real> standard: [0.2, 0, 0, 1, 1, 1]
property list<real> standardAccel: [0.3, 0, 1, 1, 1, 1]
property list<real> standardDecel: [0, 0, 0, 1, 1, 1]
@@ -36,13 +42,18 @@ JsonObject {
component AnimDurations: JsonObject {
property int expressiveDefaultSpatial: 500 * scale
property int expressiveEffects: 200 * scale
property int expressiveFastEffects: 150 * scale
property int expressiveFastSpatial: 350 * scale
property int expressiveSlowEffects: 300 * scale
property int extraLarge: 1000 * scale
property int large: 600 * scale
property int normal: 400 * scale
property real scale: 1
property int small: 200 * scale
}
component Deform: JsonObject {
property real scale: 1
}
component FontFamily: JsonObject {
property string clock: "Rubik"
property string material: "Material Symbols Rounded"
@@ -52,7 +63,8 @@ JsonObject {
component FontSize: JsonObject {
property int extraLarge: 28 * scale
property int large: 18 * scale
property int larger: 15 * scale
property int larger: 16 * scale
property int medium: 14 * scale
property int normal: 13 * scale
property real scale: 1
property int small: 11 * scale
@@ -65,28 +77,33 @@ JsonObject {
}
}
component Padding: JsonObject {
property int large: 15 * scale
property int extraLargeIncreased: 32 * scale
property int extraSmall: 4 * scale
property int large: 16 * scale
property int larger: 12 * scale
property int normal: 10 * scale
property int normal: 8 * scale
property real scale: 1
property int small: 5 * scale
property int smaller: 7 * scale
property int smallest: 2 * scale
}
component Rounding: JsonObject {
property int extraSmall: 4 * scale
property int full: 1000 * scale
property int large: 25 * scale
property int normal: 17 * scale
property int large: 24 * scale
property int medium: 16 * scale
property int normal: 18 * scale
property real scale: 1
property int small: 12 * scale
property int smallest: 8 * scale
}
component Spacing: JsonObject {
property int extraSmall: 4 * scale
property int large: 20 * scale
property int larger: 15 * scale
property int larger: 16 * scale
property int normal: 12 * scale
property real scale: 1
property int small: 7 * scale
property int small: 8 * scale
property int smaller: 10 * scale
}
component Transparency: JsonObject {
+7
View File
@@ -4,4 +4,11 @@ import qs.Config
JsonObject {
property bool enabled: true
property int wallFadeDuration: MaterialEasing.standardTime
property real alignX: 0.5
property real alignY: 0.5
property real zoom: 1.0
property real sourceClipX: 0
property real sourceClipY: 0
property real sourceClipW: 0
property real sourceClipH: 0
}
+10 -11
View File
@@ -8,10 +8,6 @@ JsonObject {
id: "workspaces",
enabled: true
},
{
id: "audio",
enabled: true
},
{
id: "media",
enabled: true
@@ -24,10 +20,6 @@ JsonObject {
id: "updates",
enabled: true
},
{
id: "dash",
enabled: true
},
{
id: "spacer",
enabled: true
@@ -41,12 +33,12 @@ JsonObject {
enabled: true
},
{
id: "tray",
id: "hyprsunset",
enabled: true
},
{
id: "upower",
enabled: false
id: "tray",
enabled: true
},
{
id: "network",
@@ -62,9 +54,13 @@ JsonObject {
},
]
property int height: 34
property bool hideWhenNotif: false
property Popouts popouts: Popouts {
}
property int rounding: 8
property int smoothing: 32
property Tray tray: Tray {
}
component Popouts: JsonObject {
property bool activeWindow: true
@@ -75,4 +71,7 @@ JsonObject {
property bool tray: true
property bool upower: true
}
component Tray: JsonObject {
property int trayIconSize: 24
}
}
+8
View File
@@ -1,5 +1,13 @@
import Quickshell.Io
JsonObject {
property Presets presets: Presets {
}
property string schemeType: "vibrant"
component Presets: JsonObject {
property string accent: ""
property string name: ""
property string variant: ""
}
}
+55 -7
View File
@@ -4,8 +4,6 @@ import Quickshell
import Quickshell.Io
import ZShell
import QtQuick
import qs.Helpers
import qs.Paths
Singleton {
id: root
@@ -23,6 +21,7 @@ Singleton {
property alias osd: adapter.osd
property alias overview: adapter.overview
property bool recentlySaved: false
property alias screenshot: adapter.screenshot
property alias services: adapter.services
property alias sidebar: adapter.sidebar
property alias utilities: adapter.utilities
@@ -48,6 +47,9 @@ Singleton {
padding: {
scale: appearance.padding.scale
},
deform: {
scale: appearance.deform.scale
},
font: {
family: {
sans: appearance.font.family.sans,
@@ -77,16 +79,28 @@ Singleton {
function serializeBackground(): var {
return {
wallFadeDuration: background.wallFadeDuration,
enabled: background.enabled
enabled: background.enabled,
alignX: background.alignX,
sourceClipX: background.sourceClipX,
sourceClipY: background.sourceClipY,
sourceClipW: background.sourceClipW,
sourceClipH: background.sourceClipH,
alignY: background.alignY,
zoom: background.zoom
};
}
function serializeBar(): var {
return {
autoHide: barConfig.autoHide,
hideWhenNotif: barConfig.hideWhenNotif,
rounding: barConfig.rounding,
border: barConfig.border,
smoothing: barConfig.smoothing,
height: barConfig.height,
tray: {
trayIconSize: barConfig.tray.trayIconSize
},
popouts: {
tray: barConfig.popouts.tray,
audio: barConfig.popouts.audio,
@@ -102,7 +116,12 @@ Singleton {
function serializeColors(): var {
return {
schemeType: colors.schemeType
schemeType: colors.schemeType,
presets: {
name: colors.presets.name,
variant: colors.presets.variant,
accent: colors.presets.accent
}
};
}
@@ -121,7 +140,8 @@ Singleton {
background: serializeBackground(),
launcher: serializeLauncher(),
colors: serializeColors(),
dock: serializeDock()
dock: serializeDock(),
screenshot: serializeScreenshot()
};
}
@@ -172,11 +192,16 @@ Singleton {
return {
logo: general.logo,
wallpaperPath: general.wallpaperPath,
username: general.username,
desktopIcons: general.desktopIcons,
dateFormat: general.dateFormat,
color: {
mode: general.color.mode,
smart: general.color.smart,
scheduleDark: general.color.scheduleDark,
scheduleHyprsunset: general.color.scheduleHyprsunset,
scheduleHyprsunsetStart: general.color.scheduleHyprsunsetStart,
hyprsunsetTemp: general.color.hyprsunsetTemp,
scheduleHyprsunsetEnd: general.color.scheduleHyprsunsetEnd,
schemeGeneration: general.color.schemeGeneration,
scheduleDarkStart: general.color.scheduleDarkStart,
scheduleDarkEnd: general.color.scheduleDarkEnd,
@@ -190,6 +215,10 @@ Singleton {
},
idle: {
timeouts: general.idle.timeouts
},
battery: {
popupThresholds: general.battery.popupThresholds,
critPerc: general.battery.critPerc
}
};
}
@@ -198,6 +227,7 @@ Singleton {
return {
maxAppsShown: launcher.maxAppsShown,
maxWallpapers: launcher.maxWallpapers,
uwsm: launcher.uwsm,
actionPrefix: launcher.actionPrefix,
specialPrefix: launcher.specialPrefix,
useFuzzy: {
@@ -221,6 +251,8 @@ Singleton {
return {
recolorLogo: lock.recolorLogo,
enableFprint: lock.enableFprint,
showNotifContent: lock.showNotifContent,
showNotifIcon: lock.showNotifIcon,
maxFprintTries: lock.maxFprintTries,
blurAmount: lock.blurAmount,
sizes: {
@@ -262,9 +294,24 @@ Singleton {
};
}
function serializeScreenshot(): var {
return {
enable_pp: screenshot.enable_pp,
mode: screenshot.mode,
radius: screenshot.radius,
shadow: screenshot.shadow,
rounding: screenshot.rounding,
shadow_blur: screenshot.shadow_blur,
shadow_color: screenshot.shadow_color,
shadow_offset_x: screenshot.shadow_offset_x,
shadow_offset_y: screenshot.shadow_offset_y
};
}
function serializeServices(): var {
return {
weatherLocation: services.weatherLocation,
updates: services.updates,
useFahrenheit: services.useFahrenheit,
ddcutilService: services.ddcutilService,
useTwelveHourClock: services.useTwelveHourClock,
@@ -317,7 +364,6 @@ Singleton {
ElapsedTimer {
id: timer
}
Timer {
@@ -415,6 +461,8 @@ Singleton {
}
property Overview overview: Overview {
}
property Screenshot screenshot: Screenshot {
}
property Services services: Services {
}
property SidebarConfig sidebar: SidebarConfig {
+12 -64
View File
@@ -29,9 +29,10 @@ Singleton {
readonly property alias wallLuminance: analyser.luminance
function alterColor(c: color, a: real, layer: int): color {
const luminance = getLuminance(c);
const initLuminance = getLuminance(c);
const luminance = Math.max(initLuminance, 0.001);
const offset = (!light || layer == 1 ? 1 : -layer / 2) * (light ? 0.2 : 0.3) * (1 - transparency.base) * (1 + wallLuminance * (light ? (layer == 1 ? 3 : 1) : 2.5));
const offset = (!light || layer == 1 ? 1 : -layer / 2) * (light ? 0.2 : 0.3) * (0.2 + 0.3 * (1 - transparency.base)) * (1 + wallLuminance * (light ? (layer == 1 ? 3 : 1) : 2.5));
const scale = (luminance + offset) / luminance;
const r = Math.max(0, Math.min(1, c.r * scale));
const g = Math.max(0, Math.min(1, c.g * scale));
@@ -84,6 +85,10 @@ Singleton {
Config.save();
}
function swapRG(c: color): color {
return Qt.rgba(c.g, c.r, c.b, c.a);
}
FileView {
path: "/etc/zshell-greeter/scheme.json"
watchChanges: true
@@ -95,69 +100,9 @@ Singleton {
ImageAnalyser {
id: analyser
source: WallpaperPath.currentWallpaperPath
source: WallpaperPath.lockscreenBg
}
component M3MaccchiatoPalette: QtObject {
property color m3background: "#131317"
property color m3error: "#ffb4ab"
property color m3errorContainer: "#93000a"
property color m3inverseOnSurface: "#303034"
property color m3inversePrimary: "#525b92"
property color m3inverseSurface: "#e4e1e7"
property color m3neutral_paletteKeyColor: "#77767b"
property color m3neutral_variant_paletteKeyColor: "#767680"
property color m3onBackground: "#e4e1e7"
property color m3onError: "#690005"
property color m3onErrorContainer: "#ffdad6"
property color m3onPrimary: "#232c60"
property color m3onPrimaryContainer: "#ffffff"
property color m3onPrimaryFixed: "#0b154b"
property color m3onPrimaryFixedVariant: "#3a4378"
property color m3onSecondary: "#2c2f44"
property color m3onSecondaryContainer: "#b1b3ce"
property color m3onSecondaryFixed: "#171a2e"
property color m3onSecondaryFixedVariant: "#42455c"
property color m3onSuccess: "#213528"
property color m3onSuccessContainer: "#D1E9D6"
property color m3onSurface: "#e4e1e7"
property color m3onSurfaceVariant: "#c6c5d1"
property color m3onTertiary: "#4c1f48"
property color m3onTertiaryContainer: "#000000"
property color m3onTertiaryFixed: "#340831"
property color m3onTertiaryFixedVariant: "#66365f"
property color m3outline: "#90909a"
property color m3outlineVariant: "#46464f"
property color m3primary: "#bac3ff"
property color m3primaryContainer: "#6a73ac"
property color m3primaryFixed: "#dee0ff"
property color m3primaryFixedDim: "#bac3ff"
property color m3primary_paletteKeyColor: "#6a73ac"
property color m3scrim: "#000000"
property color m3secondary: "#c3c5e0"
property color m3secondaryContainer: "#42455c"
property color m3secondaryFixed: "#dfe1fd"
property color m3secondaryFixedDim: "#c3c5e0"
property color m3secondary_paletteKeyColor: "#72758e"
property color m3shadow: "#000000"
property color m3success: "#B5CCBA"
property color m3successContainer: "#374B3E"
property color m3surface: "#131317"
property color m3surfaceBright: "#39393d"
property color m3surfaceContainer: "#1f1f23"
property color m3surfaceContainerHigh: "#2a2a2e"
property color m3surfaceContainerHighest: "#353438"
property color m3surfaceContainerLow: "#1b1b1f"
property color m3surfaceContainerLowest: "#0e0e12"
property color m3surfaceDim: "#131317"
property color m3surfaceTint: "#bac3ff"
property color m3surfaceVariant: "#46464f"
property color m3tertiary: "#f1b3e5"
property color m3tertiaryContainer: "#b77ead"
property color m3tertiaryFixed: "#ffd7f4"
property color m3tertiaryFixedDim: "#f1b3e5"
property color m3tertiary_paletteKeyColor: "#9b6592"
}
component M3Palette: QtObject {
property color m3background: "#191114"
property color m3error: "#ffb4ab"
@@ -279,8 +224,11 @@ Singleton {
readonly property color m3tertiary_paletteKeyColor: root.layer(root.palette.m3tertiary_paletteKeyColor)
}
component Transparency: QtObject {
readonly property real base: Appearance.transparency.base - (root.light ? 0.1 : 0)
readonly property real base: Math.max(0, Math.min(1, Appearance.transparency.base - (root.light ? 0.1 : 0)))
readonly property bool enabled: Appearance.transparency.enabled
readonly property real layers: Appearance.transparency.layers
onBaseChanged: debounceTimer.restart()
onEnabledChanged: debounceTimer.restart()
}
}
+19 -1
View File
@@ -4,13 +4,15 @@ import Quickshell
JsonObject {
property Apps apps: Apps {
}
property Battery battery: Battery {
}
property Color color: Color {
}
property string dateFormat: "ddd d MMM - hh:mm:ss"
property bool desktopIcons: false
property Idle idle: Idle {
}
property string logo: ""
property string username: ""
property string wallpaperPath: Quickshell.env("HOME") + "/Pictures/Wallpapers"
component Apps: JsonObject {
@@ -19,11 +21,27 @@ JsonObject {
property list<string> playback: ["mpv"]
property list<string> terminal: ["kitty"]
}
component Battery: JsonObject {
property int critPerc: 5
property list<var> popupThresholds: [
{
perc: 20,
name: qsTr("Low battery"),
message: qsTr("Battery is low"),
icon: "battery_android_frame_2"
},
]
}
component Color: JsonObject {
property int hyprsunsetTemp: 5000
property string mode: "dark"
property bool neovimColors: false
property bool scheduleDark: false
property int scheduleDarkEnd: 0
property int scheduleDarkStart: 0
property bool scheduleHyprsunset: false
property int scheduleHyprsunsetEnd: 0
property int scheduleHyprsunsetStart: 0
property bool schemeGeneration: true
property bool smart: false
}
+1
View File
@@ -91,6 +91,7 @@ JsonObject {
property string specialPrefix: "@"
property UseFuzzy useFuzzy: UseFuzzy {
}
property bool uwsm: true
component Sizes: JsonObject {
property int itemHeight: 50
+2
View File
@@ -5,6 +5,8 @@ JsonObject {
property bool enableFprint: true
property int maxFprintTries: 3
property bool recolorLogo: false
property bool showNotifContent: false
property bool showNotifIcon: true
property Sizes sizes: Sizes {
}
+13
View File
@@ -0,0 +1,13 @@
import Quickshell.Io
JsonObject {
property bool enable_pp: true
property string mode: "manual"
property real radius: 12.0
property bool rounding: false
property bool shadow: true
property real shadow_blur: 22.0
property list<int> shadow_color: [0, 0, 0, 160]
property real shadow_offset_x: 5.0
property real shadow_offset_y: 5.0
}
+1
View File
@@ -14,6 +14,7 @@ JsonObject {
"to": "YT Music"
}
]
property bool updates: true
property bool useFahrenheit: false
property bool useTwelveHourClock: Qt.locale().timeFormat(Locale.ShortFormat).toLowerCase().includes("a")
property int visualizerBars: 30
+1 -1
View File
@@ -83,7 +83,7 @@ ColumnLayout {
radius: Appearance.rounding.normal - Appearance.padding.smaller
StateLayer {
function onClicked(): void {
onClicked: {
root.greeter.sessionIndex = index;
}
}
+1 -1
View File
@@ -84,7 +84,7 @@ ColumnLayout {
radius: Appearance.rounding.normal - Appearance.padding.smaller
StateLayer {
function onClicked(): void {
onClicked: {
root.greeter.selectUser(modelData.username);
}
}
+30 -84
View File
@@ -1,19 +1,13 @@
pragma Singleton
import Quickshell
import Quickshell.Io
import "../scripts/levendist.js" as Levendist
import qs.Helpers
import "../scripts/fuzzysort.js" as Fuzzy
import qs.Config
Singleton {
id: root
readonly property list<DesktopEntry> list: Array.from(DesktopEntries.applications.values).filter((app, index, self) => index === self.findIndex(t => (t.id === app.id)))
readonly property var preppedIcons: list.map(a => ({
name: Fuzzy.prepare(`${a.icon} `),
entry: a
}))
readonly property list<DesktopEntry> list: Array.from(DesktopEntries.applications.values)
readonly property var preppedNames: list.map(a => ({
name: Fuzzy.prepare(`${a.name} `),
entry: a
@@ -36,8 +30,7 @@ Singleton {
"replace": "system-lock-screen"
}
]
readonly property real scoreGapThreshold: 0.1
readonly property real scoreThreshold: 0.6
property real scoreThreshold: 0.2
property var substitutions: ({
"code-url-handler": "visual-studio-code",
"Code": "visual-studio-code",
@@ -45,64 +38,27 @@ Singleton {
"pavucontrol-qt": "pavucontrol",
"wps": "wps-office2019-kprometheus",
"wpsoffice": "wps-office2019-kprometheus",
"footclient": "foot"
"footclient": "foot",
"zen": "zen-browser"
})
function bestFuzzyEntry(search: string, preppedList: list<var>, key: string): var {
const results = Fuzzy.go(search, preppedList, {
key: key,
threshold: root.scoreThreshold,
limit: 2
signal reload
function fuzzyQuery(search: string): var {
return Fuzzy.go(search, preppedNames, {
all: true,
key: "name"
}).map(r => {
return r.obj.entry;
});
if (!results || results.length === 0)
return null;
const best = results[0];
const second = results.length > 1 ? results[1] : null;
if (second && (best.score - second.score) < root.scoreGapThreshold)
return null;
return best.obj.entry;
}
function fuzzyQuery(search: string, preppedList: list<var>): var {
const entry = bestFuzzyEntry(search, preppedList, "name");
return entry ? [entry] : [];
}
function getKebabNormalizedAppName(str: string): string {
return str.toLowerCase().replace(/\s+/g, "-");
}
function getReverseDomainNameAppName(str: string): string {
return str.split('.').slice(-1)[0];
}
function getUndescoreToKebabAppName(str: string): string {
return str.toLowerCase().replace(/_/g, "-");
}
function guessIcon(str) {
if (!str || str.length == 0)
return "image-missing";
if (iconExists(str))
return str;
const entry = DesktopEntries.byId(str);
if (entry)
return entry.icon;
const heuristicEntry = DesktopEntries.heuristicLookup(str);
if (heuristicEntry)
return heuristicEntry.icon;
if (substitutions[str])
return substitutions[str];
if (substitutions[str.toLowerCase()])
return substitutions[str.toLowerCase()];
for (let i = 0; i < regexSubstitutions.length; i++) {
const substitution = regexSubstitutions[i];
@@ -111,35 +67,25 @@ Singleton {
return replacedName;
}
const lowercased = str.toLowerCase();
if (iconExists(lowercased))
return lowercased;
if (iconExists(str))
return str;
const reverseDomainNameAppName = getReverseDomainNameAppName(str);
if (iconExists(reverseDomainNameAppName))
return reverseDomainNameAppName;
let guessStr = str;
guessStr = str.split('.').slice(-1)[0].toLowerCase();
if (iconExists(guessStr))
return guessStr;
guessStr = str.toLowerCase().replace(/\s+/g, "-");
if (iconExists(guessStr))
return guessStr;
const searchResults = root.fuzzyQuery(str);
if (searchResults.length > 0) {
const firstEntry = searchResults[0];
guessStr = firstEntry.icon;
if (iconExists(guessStr))
return guessStr;
}
const lowercasedDomainNameAppName = reverseDomainNameAppName.toLowerCase();
if (iconExists(lowercasedDomainNameAppName))
return lowercasedDomainNameAppName;
const kebabNormalizedGuess = getKebabNormalizedAppName(str);
if (iconExists(kebabNormalizedGuess))
return kebabNormalizedGuess;
const undescoreToKebabGuess = getUndescoreToKebabAppName(str);
if (iconExists(undescoreToKebabGuess))
return undescoreToKebabGuess;
const iconSearchResult = fuzzyQuery(str, preppedIcons);
if (iconSearchResult && iconExists(iconSearchResult.icon))
return iconSearchResult.icon;
const nameSearchResult = root.fuzzyQuery(str, preppedNames);
if (nameSearchResult && iconExists(nameSearchResult.icon))
return nameSearchResult.icon;
return "application-x-executable";
return str;
}
function iconExists(iconName) {
+2
View File
@@ -29,4 +29,6 @@ Singleton {
readonly property bool isLaptop: UPower.displayDevice.isLaptopBattery
readonly property bool onBattery: UPower.onBattery
readonly property bool ready: UPower.displayDevice.ready
readonly property real timeToEmpty: UPower.displayDevice.timeToEmpty
readonly property real timeToFull: UPower.displayDevice.timeToFull
}
+1 -1
View File
@@ -232,7 +232,7 @@ Singleton {
}
function setBrightness(value: real): void {
value = Math.max(0, Math.min(1, value));
value = Math.max(Config.services.minBrightness, Math.min(1, value));
const rounded = Math.round(value * 100);
if (Math.round(brightness * 100) === rounded)
return;
+249
View File
@@ -0,0 +1,249 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Config
import "../scripts/fuzzysort.js" as Fuzzy
Singleton {
id: root
property string cliphistBinary: "cliphist"
property string currentEntry: ""
property list<string> entries: []
property real pasteDelay: 0.05
readonly property var preparedEntries: entries.map(a => ({
name: Fuzzy.prepare(displayText(a)),
entry: a
}))
property string previewImageFile: "/tmp/qs-cliphist-preview.img"
property string previewImageSource: ""
property bool previewIsImage: false
property list<var> previewText: []
property int previewToken: 0
property real scoreThreshold: 0.2
function copy(entry): void {
Quickshell.execDetached(["bash", "-c", `printf '${shellSingleQuoteEscape(entry)}' | ${root.cliphistBinary} decode | wl-copy`]);
}
function deleteEntry(entry): void {
deleteProc.deleteEntry(entry);
}
function displayText(entry): string {
return entry.replace(/^\s*\d+\s+/, "");
}
function entryIsImage(entry): bool {
return !!(/^\d+\t\[\[.*binary data.*\d+x\d+.*\]\]$/.test(entry));
}
function escapeHtml(str): string {
return String(str).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
}
function fuzzyQuery(search: string): var {
if (search.trim() === "") {
return entries;
}
return Fuzzy.go(search, preparedEntries, {
all: true,
key: "name"
}).map(r => {
return r.obj.entry;
});
}
function paste(entry): void {
Quickshell.execDetached(["bash", "-c", `printf '${shellSingleQuoteEscape(entry)}' | ${root.cliphistBinary} decode | wl-copy && wl-paste`]);
}
function processPreviewLines(text: string): var {
if (text === "")
return [];
const lines = text.split("\n");
let minIndent = Infinity;
for (let i = 0; i < lines.length; i++) {
const m = lines[i].match(/^(\s*)/);
const indent = m ? m[1].length : 0;
if (lines[i].trim().length > 0 && indent < minIndent)
minIndent = indent;
}
if (minIndent === Infinity)
minIndent = 0;
const result = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const indentMatch = line.match(/^(\s*)/);
const indentLen = indentMatch ? indentMatch[1].length : 0;
const stripped = line.slice(Math.min(minIndent, indentLen));
result.push({
line: i + 1,
text: stripped
});
}
return result;
}
function refresh(): void {
readProc.buffer = [];
readProc.running = true;
}
function refreshPreview(): void {
previewToken += 1;
const token = previewToken;
if (!currentEntry) {
previewText = [];
previewImageSource = "";
previewIsImage = false;
return;
}
previewImageSource = "";
previewIsImage = entryIsImage(currentEntry);
if (previewIsImage) {
previewImageProc.token = token;
previewImageProc.running = true;
} else {
previewTextProc.token = token;
previewTextProc.running = true;
}
}
function shellSingleQuoteEscape(str): string {
return String(str).replace(/'/g, "'\\''");
}
function wipe(): void {
wipeProc.running = true;
}
Process {
id: deleteProc
property string entry: ""
function deleteEntry(entry) {
deleteProc.entry = entry;
deleteProc.running = true;
deleteProc.entry = "";
}
command: ["bash", "-c", `echo '${root.shellSingleQuoteEscape(deleteProc.entry)}' | ${root.cliphistBinary} delete`]
onExited: (exitCode, exitStatus) => {
root.refresh();
}
}
Process {
id: wipeProc
command: [root.cliphistBinary, "wipe"]
onExited: (exitCode, exitStatus) => {
root.refresh();
}
}
Connections {
function onClipboardTextChanged() {
delayedUpdateTimer.restart();
}
target: Quickshell
}
Timer {
id: delayedUpdateTimer
interval: 50
repeat: false
onTriggered: {
root.refresh();
}
}
Process {
id: previewTextProc
property int token: 0
command: ["bash", "-c", `
printf '%s' '${root.shellSingleQuoteEscape(root.currentEntry)}' | ${root.cliphistBinary} decode
`]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (previewTextProc.token !== root.previewToken)
return;
root.previewText = root.processPreviewLines(this.text);
}
}
}
Process {
id: previewImageProc
property int token: 0
command: ["bash", "-c", `
set -euo pipefail
tmp='${root.shellSingleQuoteEscape(root.previewImageFile)}'
printf '%s' '${root.shellSingleQuoteEscape(root.currentEntry)}' | ${root.cliphistBinary} decode > "$tmp"
`]
running: false
onExited: (exitCode, exitStatus) => {
if (token !== root.previewToken)
return;
if (exitCode !== 0) {
console.error("[Cliphist] image preview failed", exitCode, exitStatus);
return;
}
root.previewImageSource = "";
Qt.callLater(() => {
root.previewImageSource = `file://${root.previewImageFile}`;
});
}
}
Process {
id: readProc
property list<string> buffer: []
command: [root.cliphistBinary, "list"]
stdout: SplitParser {
onRead: line => {
readProc.buffer.push(line);
}
}
onExited: (exitCode, exitStatus) => {
if (exitCode === 0) {
root.entries = readProc.buffer;
} else {
console.error("[Cliphist] Failed to refresh with code", exitCode, "and status", exitStatus);
}
}
}
IpcHandler {
function update(): void {
root.refresh();
}
target: "cliphistService"
}
}
+28 -4
View File
@@ -42,11 +42,24 @@ Singleton {
function checkStartup() {
if (!root.enabled)
return;
var now = new Date();
if (now.getHours() >= darkStart || now.getHours() < darkEnd) {
applyDarkMode();
var nowMinutes = now.getHours() * 60 + now.getMinutes();
var isDarkTime;
if (root.darkStart <= root.darkEnd) {
isDarkTime = (nowMinutes >= root.darkStart && nowMinutes < root.darkEnd);
} else {
applyLightMode();
isDarkTime = (nowMinutes >= root.darkStart || nowMinutes < root.darkEnd);
}
if (isDarkTime) {
if (DynamicColors.light)
root.applyDarkMode();
} else {
if (!DynamicColors.light)
root.applyLightMode();
}
}
@@ -60,8 +73,19 @@ Singleton {
onTriggered: {
if (!root.enabled)
return;
var now = new Date();
if (now.getHours() >= root.darkStart || now.getHours() < root.darkEnd) {
var nowMinutes = now.getHours() * 60 + now.getMinutes();
var isDarkTime;
if (root.darkStart <= root.darkEnd) {
isDarkTime = (nowMinutes >= root.darkStart && nowMinutes < root.darkEnd);
} else {
isDarkTime = (nowMinutes >= root.darkStart || nowMinutes < root.darkEnd);
}
if (isDarkTime) {
if (DynamicColors.light)
root.applyDarkMode();
} else {
+21 -1
View File
@@ -39,6 +39,21 @@ Singleton {
readonly property real uploadSpeed: _uploadSpeed
readonly property real uploadTotal: _uploadTotal
function resetSampling(): void {
netDevFile.reload();
const content = netDevFile.text();
if (!content)
return;
const data = root.parseNetDev(content);
const now = Date.now();
root._prevRxBytes = data.rx;
root._prevTxBytes = data.tx;
root._prevTimestamp = now;
}
function formatBytes(bytes: real): var {
// Handle negative or invalid values
if (bytes < 0 || isNaN(bytes) || !isFinite(bytes)) {
@@ -156,10 +171,15 @@ Singleton {
Timer {
interval: Config.dashboard.resourceUpdateInterval
repeat: true
running: root.refCount > 0
running: true
triggeredOnStart: true
onTriggered: {
if (root.refCount <= 0) {
root.resetSampling();
return;
}
netDevFile.reload();
const content = netDevFile.text();
if (!content)
-464
View File
@@ -1,464 +0,0 @@
pragma Singleton
import Quickshell
import Quickshell.Io
import QtQuick
import qs.Config
Singleton {
id: root
property string autoGpuType: "NONE"
property string cpuName: ""
property real cpuPerc
property real cpuTemp
// Individual disks: Array of { mount, used, total, free, perc }
property var disks: []
property real gpuMemTotal: 0
property real gpuMemUsed
property string gpuName
property real gpuPerc
property real gpuTemp
readonly property string gpuType: Config.services.gpuType.toUpperCase() || autoGpuType
property real lastCpuIdle
property real lastCpuTotal
readonly property real memPerc: memTotal > 0 ? memUsed / memTotal : 0
property real memTotal
property real memUsed
property int refCount
readonly property real storagePerc: {
let totalUsed = 0;
let totalSize = 0;
for (const disk of disks) {
totalUsed += disk.used;
totalSize += disk.total;
}
return totalSize > 0 ? totalUsed / totalSize : 0;
}
function cleanCpuName(name: string): string {
return name.replace(/\(R\)/gi, "").replace(/\(TM\)/gi, "").replace(/CPU/gi, "").replace(/\d+th Gen /gi, "").replace(/\d+nd Gen /gi, "").replace(/\d+rd Gen /gi, "").replace(/\d+st Gen /gi, "").replace(/Core /gi, "").replace(/Processor/gi, "").replace(/\s+/g, " ").trim();
}
function cleanGpuName(name: string): string {
return name.replace(/NVIDIA GeForce /gi, "").replace(/NVIDIA /gi, "").replace(/AMD Radeon /gi, "").replace(/AMD /gi, "").replace(/Intel /gi, "").replace(/\(R\)/gi, "").replace(/\(TM\)/gi, "").replace(/Graphics/gi, "").replace(/\s+/g, " ").trim();
}
function formatKib(kib: real): var {
const mib = 1024;
const gib = 1024 ** 2;
const tib = 1024 ** 3;
if (kib >= tib)
return {
value: kib / tib,
unit: "TiB"
};
if (kib >= gib)
return {
value: kib / gib,
unit: "GiB"
};
if (kib >= mib)
return {
value: kib / mib,
unit: "MiB"
};
return {
value: kib,
unit: "KiB"
};
}
Timer {
interval: Config.dashboard.resourceUpdateInterval
repeat: true
running: root.refCount > 0
triggeredOnStart: true
onTriggered: {
stat.reload();
meminfo.reload();
if (root.gpuType === "GENERIC")
gpuUsage.running = true;
if (root.gpuType === "GENERIC" && root.gpuMemTotal === 0)
oneshotMemAmd.running = true;
}
}
Timer {
interval: 60000 * 120
repeat: true
running: true
triggeredOnStart: true
onTriggered: {
storage.running = true;
}
}
Timer {
interval: Config.dashboard.resourceUpdateInterval * 5
repeat: true
running: root.refCount > 0
triggeredOnStart: true
onTriggered: {
sensors.running = true;
}
}
FileView {
id: cpuinfoInit
path: "/proc/cpuinfo"
onLoaded: {
const nameMatch = text().match(/model name\s*:\s*(.+)/);
if (nameMatch)
root.cpuName = root.cleanCpuName(nameMatch[1]);
}
}
FileView {
id: stat
path: "/proc/stat"
onLoaded: {
const data = text().match(/^cpu\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/);
if (data) {
const stats = data.slice(1).map(n => parseInt(n, 10));
const total = stats.reduce((a, b) => a + b, 0);
const idle = stats[3] + (stats[4] ?? 0);
const totalDiff = total - root.lastCpuTotal;
const idleDiff = idle - root.lastCpuIdle;
const newCpuPerc = totalDiff > 0 ? (1 - idleDiff / totalDiff) : 0;
root.lastCpuTotal = total;
root.lastCpuIdle = idle;
if (Math.abs(newCpuPerc - root.cpuPerc) >= 0.01)
root.cpuPerc = newCpuPerc;
}
}
}
FileView {
id: meminfo
path: "/proc/meminfo"
onLoaded: {
const data = text();
const total = parseInt(data.match(/MemTotal: *(\d+)/)[1], 10) || 1;
const used = (root.memTotal - parseInt(data.match(/MemAvailable: *(\d+)/)[1], 10)) || 0;
if (root.memTotal !== total)
root.memTotal = total;
if (Math.abs(used - root.memUsed) >= 16384)
root.memUsed = used;
}
}
Process {
id: storage
command: ["lsblk", "-b", "-o", "NAME,SIZE,TYPE,FSUSED,FSSIZE", "-P"]
stdout: StdioCollector {
onStreamFinished: {
const diskMap = {}; // Map disk name -> { name, totalSize, used, fsTotal }
const lines = text.trim().split("\n");
for (const line of lines) {
if (line.trim() === "")
continue;
const nameMatch = line.match(/NAME="([^"]+)"/);
const sizeMatch = line.match(/SIZE="([^"]+)"/);
const typeMatch = line.match(/TYPE="([^"]+)"/);
const fsusedMatch = line.match(/FSUSED="([^"]*)"/);
const fssizeMatch = line.match(/FSSIZE="([^"]*)"/);
if (!nameMatch || !typeMatch)
continue;
const name = nameMatch[1];
const type = typeMatch[1];
const size = parseInt(sizeMatch?.[1] || "0", 10);
const fsused = parseInt(fsusedMatch?.[1] || "0", 10);
const fssize = parseInt(fssizeMatch?.[1] || "0", 10);
if (type === "disk") {
// Skip zram (swap) devices
if (name.startsWith("zram"))
continue;
// Initialize disk entry
if (!diskMap[name]) {
diskMap[name] = {
name: name,
totalSize: size,
used: 0,
fsTotal: 0
};
}
} else if (type === "part") {
// Find parent disk (remove trailing numbers/p+numbers)
let parentDisk = name.replace(/p?\d+$/, "");
// For nvme devices like nvme0n1p1, parent is nvme0n1
if (name.match(/nvme\d+n\d+p\d+/))
parentDisk = name.replace(/p\d+$/, "");
// Aggregate partition usage to parent disk
if (diskMap[parentDisk]) {
diskMap[parentDisk].used += fsused;
diskMap[parentDisk].fsTotal += fssize;
}
}
}
const diskList = [];
let totalUsed = 0;
let totalSize = 0;
for (const diskName of Object.keys(diskMap).sort()) {
const disk = diskMap[diskName];
// Use filesystem total if available, otherwise use disk size
const total = disk.fsTotal > 0 ? disk.fsTotal : disk.totalSize;
const used = disk.used;
const perc = total > 0 ? used / total : 0;
// Convert bytes to KiB for consistency with formatKib
diskList.push({
mount: disk.name // Using 'mount' property for compatibility
,
used: used / 1024,
total: total / 1024,
free: (total - used) / 1024,
perc: perc
});
totalUsed += used;
totalSize += total;
}
root.disks = diskList;
}
}
}
Process {
id: gpuNameDetect
command: ["sh", "-c", "nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null || lspci 2>/dev/null | grep -i 'vga\\|3d\\|display' | head -1"]
running: true
stdout: StdioCollector {
onStreamFinished: {
const output = text.trim();
if (!output)
return;
// Check if it's from nvidia-smi (clean GPU name)
if (output.toLowerCase().includes("nvidia") || output.toLowerCase().includes("geforce") || output.toLowerCase().includes("rtx") || output.toLowerCase().includes("gtx")) {
root.gpuName = root.cleanGpuName(output);
} else {
// Parse lspci output: extract name from brackets or after colon
const bracketMatch = output.match(/\[([^\]]+)\]/);
if (bracketMatch) {
root.gpuName = root.cleanGpuName(bracketMatch[1]);
} else {
const colonMatch = output.match(/:\s*(.+)/);
if (colonMatch)
root.gpuName = root.cleanGpuName(colonMatch[1]);
}
}
}
}
}
Process {
id: gpuTypeCheck
command: ["sh", "-c", "if command -v nvidia-smi &>/dev/null && nvidia-smi -L &>/dev/null; then echo NVIDIA; elif ls /sys/class/drm/card*/device/gpu_busy_percent 2>/dev/null | grep -q .; then echo GENERIC; else echo NONE; fi"]
running: !Config.services.gpuType
stdout: StdioCollector {
onStreamFinished: root.autoGpuType = text.trim()
}
}
Process {
id: oneshotMem
command: ["nvidia-smi", "--query-gpu=memory.total", "--format=csv,noheader,nounits"]
running: root.gpuType === "NVIDIA" && root.gpuMemTotal === 0
stdout: StdioCollector {
onStreamFinished: {
root.gpuMemTotal = Number(this.text.trim());
oneshotMem.running = false;
}
}
}
Process {
id: oneshotMemAmd
command: ["sh", "-c", "cat /sys/class/drm/card*/device/mem_info_vram_total"]
running: root.gpuType === "GENERIC" && root.gpuMemTotal === 0
stdout: StdioCollector {
onStreamFinished: {
const values = text.trim().split("\n").map(v => parseInt(v, 10)).filter(v => Number.isFinite(v));
if (values.length > 0) {
const totalBytes = values.reduce((a, b) => a + b, 0);
root.gpuMemTotal = totalBytes / (1024 * 1024);
}
oneshotMemAmd.running = false;
}
}
}
Process {
id: gpuUsageNvidia
command: ["/usr/bin/nvidia-smi", "--query-gpu=utilization.gpu,temperature.gpu,memory.used", "--format=csv,noheader,nounits", "-lms", "1000"]
running: root.refCount > 0 && root.gpuType === "NVIDIA"
stdout: SplitParser {
onRead: data => {
const parts = String(data).trim().split(/\s*,\s*/);
if (parts.length < 3)
return;
const usageRaw = parseInt(parts[0], 10);
const tempRaw = parseInt(parts[1], 10);
const memRaw = parseInt(parts[2], 10);
if (!Number.isFinite(usageRaw) || !Number.isFinite(tempRaw) || !Number.isFinite(memRaw))
return;
const newGpuPerc = Math.max(0, Math.min(1, usageRaw / 100));
const newGpuTemp = tempRaw;
const newGpuMemUsed = root.gpuMemTotal > 0 ? Math.max(0, Math.min(1, memRaw / root.gpuMemTotal)) : 0;
// Only publish meaningful changes to avoid needless binding churn / repaints
if (Math.abs(root.gpuPerc - newGpuPerc) >= 0.01)
root.gpuPerc = newGpuPerc;
if (Math.abs(root.gpuTemp - newGpuTemp) >= 1)
root.gpuTemp = newGpuTemp;
if (Math.abs(root.gpuMemUsed - newGpuMemUsed) >= 0.01)
root.gpuMemUsed = newGpuMemUsed;
}
}
}
Process {
id: gpuUsage
command: root.gpuType === "GENERIC" ? ["sh", "-c", "paste -d ' ' /sys/class/drm/card*/device/gpu_busy_percent /sys/class/drm/card*/device/mem_info_vram_used"] : ["echo"]
stdout: StdioCollector {
onStreamFinished: {
if (root.gpuType === "GENERIC") {
const lines = text.trim().split("\n");
let percSum = 0;
let memSum = 0;
let count = 0;
for (const line of lines) {
const parts = line.trim().split(/\s+/);
if (parts.length < 2)
continue;
const gpuBusy = parseInt(parts[0], 10);
const memUsed = parseInt(parts[1], 10);
if (!Number.isFinite(gpuBusy) || !Number.isFinite(memUsed))
continue;
percSum += gpuBusy;
memSum += memUsed;
count++;
}
if (count > 0) {
// GPU usage %
root.gpuPerc = (percSum / count) / 100;
// VRAM usage (bytes → MiB → normalized)
const memUsedMiB = memSum / (1024 * 1024);
const newGpuMemUsed = root.gpuMemTotal > 0 ? Math.max(0, Math.min(1, memUsedMiB / root.gpuMemTotal)) : 0;
if (Math.abs(root.gpuMemUsed - newGpuMemUsed) >= 0.01)
root.gpuMemUsed = newGpuMemUsed;
}
} else {
root.gpuPerc = 0;
root.gpuTemp = 0;
}
}
}
}
Process {
id: sensors
command: ["sensors"]
environment: ({
LANG: "C.UTF-8",
LC_ALL: "C.UTF-8"
})
stdout: StdioCollector {
onStreamFinished: {
let cpuTemp = text.match(/(?:Package id [0-9]+|Tdie):\s+((\+|-)[0-9.]+)(°| )C/);
if (!cpuTemp)
// If AMD Tdie pattern failed, try fallback on Tctl
cpuTemp = text.match(/Tctl:\s+((\+|-)[0-9.]+)(°| )C/);
if (cpuTemp && Math.abs(parseFloat(cpuTemp[1]) - root.cpuTemp) >= 0.5)
root.cpuTemp = parseFloat(cpuTemp[1]);
if (root.gpuType !== "GENERIC")
return;
let eligible = false;
let sum = 0;
let count = 0;
for (const line of text.trim().split("\n")) {
if (line === "Adapter: PCI adapter")
eligible = true;
else if (line === "")
eligible = false;
else if (eligible) {
let match = line.match(/^(temp[0-9]+|GPU core|edge)+:\s+\+([0-9]+\.[0-9]+)(°| )C/);
if (!match)
// Fall back to junction/mem if GPU doesn't have edge temp (for AMD GPUs)
match = line.match(/^(junction|mem)+:\s+\+([0-9]+\.[0-9]+)(°| )C/);
if (match) {
sum += parseFloat(match[2]);
count++;
}
}
}
root.gpuTemp = count > 0 ? sum / count : 0;
}
}
}
}
+15 -12
View File
@@ -32,33 +32,26 @@ Searcher {
showPreview = true;
}
function setCrop(screen: string, rect: rect, scaledRect: rect, zoom: real): void {
let updated = Object.assign({}, root.crops);
function setCrop(screen: string, rect: rect, zoom: real): void {
if (zoom <= 0)
zoom = 1.0;
else if (zoom > 5.0)
zoom = 5.0;
updated[screen] = {
root.crops[screen] = {
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
scaledX: scaledRect.x,
scaledY: scaledRect.y,
scaledWidth: scaledRect.width,
scaledHeight: scaledRect.height,
zoom: zoom
};
root.crops = updated;
// root.crops = updated;
}
function setWallpaper(path: string): void {
actualCurrent = path;
WallpaperPath.currentWallpaperPath = path;
Quickshell.screens.forEach(n => setCrop(n.name, Qt.rect(0, 0, 1, 1), Qt.rect(0, 0, 0, 0), 1.0));
Quickshell.screens.forEach(n => setCrop(n.name, Qt.rect(0, 0, 1, 1), 1.0));
Quickshell.execDetached(["zshell-cli", "wallpaper", "lockscreen", "--input-image", `${root.actualCurrent}`, "--output-path", `${Paths.state}/lockscreen_bg.png`, "--blur-amount", `${Config.lock.blurAmount}`]);
if (Config.general.color.schemeGeneration)
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--image-path", `${root.actualCurrent}`, "--scheme", `${Config.colors.schemeType}`, "--mode", `${Config.general.color.mode}`]);
@@ -91,7 +84,7 @@ Searcher {
path: `${Paths.state}/wallpaper-crops.json`
watchChanges: true
onAdapterUpdated: writeAdapter()
onAdapterUpdated: cropWriteDelay.restart()
onFileChanged: reload()
JsonAdapter {
@@ -101,6 +94,16 @@ Searcher {
}
}
Timer {
id: cropWriteDelay
interval: 100
repeat: false
running: false
onTriggered: monitorCrops.writeAdapter()
}
FileSystemModel {
id: wallpapers
+10
View File
@@ -18,6 +18,7 @@ RowLayout {
required property Wrapper popouts
required property ClipWrapper popoutsWrapper
required property ShellScreen screen
required property bool fullscreen
readonly property int vPadding: 6
required property PersistentProperties visibilities
@@ -91,6 +92,7 @@ RowLayout {
delegate: WrappedLoader {
sourceComponent: HyprsunsetWidget {
visible: !root.fullscreen
}
}
}
@@ -109,6 +111,7 @@ RowLayout {
delegate: WrappedLoader {
sourceComponent: Workspaces {
screen: root.screen
visible: !root.fullscreen
}
}
}
@@ -120,6 +123,7 @@ RowLayout {
sourceComponent: TrayWidget {
loader: root
popouts: root.popouts
visible: !root.fullscreen
}
}
}
@@ -130,6 +134,7 @@ RowLayout {
delegate: WrappedLoader {
sourceComponent: Resources {
visibilities: root.visibilities
visible: !root.fullscreen
}
}
}
@@ -139,6 +144,7 @@ RowLayout {
delegate: WrappedLoader {
sourceComponent: UpdatesWidget {
visible: !root.fullscreen
}
}
}
@@ -150,6 +156,7 @@ RowLayout {
sourceComponent: NotifBell {
popouts: root.popouts
visibilities: root.visibilities
visible: !root.fullscreen
}
}
}
@@ -162,6 +169,7 @@ RowLayout {
loader: root
popouts: root.popouts
visibilities: root.visibilities
visible: !root.fullscreen
}
}
}
@@ -172,6 +180,7 @@ RowLayout {
delegate: WrappedLoader {
sourceComponent: WindowTitle {
bar: root
visible: !root.fullscreen
}
}
}
@@ -190,6 +199,7 @@ RowLayout {
delegate: WrappedLoader {
sourceComponent: MediaWidget {
visible: !root.fullscreen
}
}
}
+4 -2
View File
@@ -17,12 +17,13 @@ Item {
readonly property int contentHeight: Config.barConfig.height + padding * 2
readonly property int exclusiveZone: Config.barConfig.autoHide ? Config.barConfig.border : contentHeight
required property bool fullscreen
property bool isHovered
readonly property int padding: Math.max(Appearance.padding.smaller, Config.barConfig.border)
required property Wrapper popouts
required property ClipWrapper popoutsWrapper
required property ShellScreen screen
readonly property bool shouldBeVisible: (!Config.barConfig.autoHide || visibilities.bar || isHovered)
readonly property bool shouldBeVisible: !fullscreen && (!Config.barConfig.autoHide || visibilities.bar || isHovered)
readonly property int vPadding: 6
required property PersistentProperties visibilities
@@ -30,7 +31,7 @@ Item {
content.item?.checkPopout(x);
}
implicitHeight: Config.barConfig.border
implicitHeight: fullscreen ? 0 : Config.barConfig.border
visible: height > Config.barConfig.border
states: State {
@@ -75,6 +76,7 @@ Item {
anchors.right: parent.right
sourceComponent: Bar {
fullscreen: root.fullscreen
height: root.contentHeight
popouts: root.popouts
popoutsWrapper: root.popoutsWrapper
+2 -7
View File
@@ -8,6 +8,7 @@ import qs.Config
Item {
id: root
required property real borderThickness
readonly property alias content: content
property real offsetScale: y > 0 || content.hasCurrent ? 0 : 1
required property ShellScreen screen
@@ -17,21 +18,15 @@ Item {
implicitWidth: content.implicitWidth
visible: width > 0 && height > 0
x: {
if (content.isDetached)
return (parent.width - content.nonAnimWidth) / 2;
const off = content.currentCenter - Config.barConfig.border - content.nonAnimWidth / 2;
const off = content.currentCenter - borderThickness - content.nonAnimWidth / 2;
const diff = parent.width - Math.floor(off + content.nonAnimWidth);
if (diff < 0)
return off + diff;
return Math.floor(Math.max(off, 0));
}
y: content.isDetached ? (parent.height - content.nonAnimHeight) / 2 : 0
Behavior on offsetScale {
Anim {
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
}
Behavior on x {
+357
View File
@@ -0,0 +1,357 @@
pragma ComponentBehavior: Bound
import Quickshell
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Components
import qs.Helpers
import qs.Config
Item {
id: root
readonly property int itemHeight: Config.clipboard.sizes.itemHeight
required property ShellScreen screen
required property PersistentProperties visibilities
implicitHeight: search.implicitHeight + entries.anchors.topMargin + ((view.spacing + itemHeight) * Config.clipboard.maxEntriesShown) - view.spacing
implicitWidth: Config.clipboard.sizes.width + preview.width + preview.anchors.leftMargin
Component.onCompleted: {
if (ClipHistory.entries.length === 0)
ClipHistory.refresh();
searchField.forceActiveFocus();
}
CustomClippingRect {
id: search
anchors.left: parent.left
anchors.right: entries.right
anchors.top: parent.top
color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: 50
radius: Appearance.rounding.full
MaterialIcon {
id: searchIcon
anchors.left: parent.left
anchors.margins: Appearance.padding.large
anchors.verticalCenter: parent.verticalCenter
text: "search"
}
CustomTextField {
id: searchField
anchors.bottom: parent.bottom
anchors.left: searchIcon.right
anchors.leftMargin: Appearance.spacing.small
anchors.right: parent.right
anchors.top: parent.top
color: DynamicColors.palette.m3onSurface
placeholderText: "Search clipboard history..."
Keys.onDownPressed: view.incrementCurrentIndex()
Keys.onUpPressed: view.decrementCurrentIndex()
onAccepted: {
ClipHistory.copy(view.currentItem.modelData);
root.visibilities.clipboard = false;
}
}
}
CustomClippingRect {
id: preview
anchors.bottom: parent.bottom
anchors.left: entries.right
anchors.leftMargin: Appearance.spacing.normal
anchors.top: parent.top
color: DynamicColors.tPalette.m3surfaceContainer
implicitWidth: ClipHistory.previewIsImage ? Math.max(Math.min(imagePreview.sourceSize.width + Appearance.padding.large * 2, Config.clipboard.sizes.previewWidth), Config.clipboard.sizes.minPreviewWidth) : Math.max(Math.min(textPreviewColumn.width + textPreviewColumn.anchors.margins * 2, Config.clipboard.sizes.previewWidth), Config.clipboard.sizes.minPreviewWidth)
radius: 25
Behavior on implicitWidth {
Anim {
}
}
Column {
id: textPreviewColumn
anchors.left: parent.left
anchors.leftMargin: 0
anchors.margins: Appearance.padding.normal
anchors.top: parent.top
visible: !ClipHistory.previewIsImage
Repeater {
id: processedLines
model: ClipHistory.previewText
Row {
id: lineRow
required property int index
required property var modelData
spacing: Appearance.spacing.normal
CustomRect {
color: lineRow.index % 2 ? DynamicColors.tPalette.m3surfaceContainer : "transparent"
implicitHeight: lineText.paintedHeight + Appearance.padding.extraSmall * 2
implicitWidth: 50
CustomText {
id: number
anchors.margins: Appearance.padding.large
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
text: lineRow.modelData.line
}
}
Repeater {
model: lineRow.modelData.text.split("\t").slice(1)
RowLayout {
height: lineText.height
width: lineText.height + Appearance.spacing.extraSmall
Item {
Layout.fillHeight: true
Layout.fillWidth: true
CustomRect {
anchors.centerIn: parent
color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: parent.height / 8
implicitWidth: parent.height / 8
radius: Appearance.rounding.full
}
}
Item {
Layout.fillHeight: true
Layout.fillWidth: true
CustomRect {
anchors.centerIn: parent
color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: parent.height / 8
implicitWidth: parent.height / 8
radius: Appearance.rounding.full
}
}
Item {
Layout.fillHeight: true
Layout.fillWidth: true
CustomRect {
anchors.centerIn: parent
color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: parent.height / 8
implicitWidth: parent.height / 8
radius: Appearance.rounding.full
}
}
}
}
CustomText {
id: lineText
color: DynamicColors.palette.m3onSurface
font.family: ClipHistory.previewIsCode ? Appearance.font.family.mono : Appearance.font.family.sans
height: lineText.paintedHeight + Appearance.padding.extraSmall * 2
text: lineRow.modelData.text.trim()
verticalAlignment: Text.AlignVCenter
wrapMode: Text.Wrap
}
}
}
}
Image {
id: imagePreview
anchors.centerIn: parent
asynchronous: true
cache: false
fillMode: Image.PreserveAspectFit
mipmap: true
retainWhileLoading: true
smooth: true
source: ClipHistory.previewImageSource
visible: ClipHistory.previewIsImage && ClipHistory.previewImageSource !== ""
width: Math.min(sourceSize.width, Config.clipboard.sizes.previewWidth)
}
}
Item {
id: entries
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.top: search.bottom
anchors.topMargin: Appearance.spacing.normal
implicitWidth: Config.clipboard.sizes.width
CustomListView {
id: view
anchors.fill: parent
cacheBuffer: (root.itemHeight + spacing) * 2
highlightFollowsCurrentItem: false
highlightRangeMode: ListView.ApplyRange
preferredHighlightBegin: 0
preferredHighlightEnd: height
spacing: Appearance.spacing.normal
CustomScrollBar.vertical: CustomScrollBar {
flickable: view
minimumSize: 0.1
}
delegate: CustomRect {
id: clipItem
readonly property bool isImage: ClipHistory.entryIsImage(modelData)
required property string modelData
implicitHeight: root.itemHeight
implicitWidth: view.width
radius: textLayer.pressed ? (Appearance.rounding.small / 2) : Appearance.rounding.small
Behavior on radius {
Anim {
type: Anim.FastEffects
}
}
RowLayout {
anchors.fill: parent
spacing: Appearance.spacing.small
CustomClippingRect {
id: textRect
Layout.fillHeight: true
Layout.fillWidth: true
Item {
id: textWrapper
anchors.fill: parent
layer.enabled: true
layer.effect: OpacityMask {
maskSource: fadeMask
}
MaterialIcon {
id: icon
anchors.left: parent.left
anchors.margins: Appearance.padding.normal
anchors.verticalCenter: parent.verticalCenter
font.pointSize: Appearance.font.size.large
text: clipItem.isImage ? "image" : "text_fields"
}
CustomText {
id: text
anchors.left: icon.right
anchors.margins: Appearance.spacing.normal
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
text: clipItem.isImage ? qsTr("Image") : ClipHistory.displayText(clipItem.modelData)
}
}
CustomRect {
id: fadeMask
anchors.fill: parent
layer.enabled: true
visible: false
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop {
color: Qt.rgba(1, 1, 1, 1.0)
position: 0.85
}
GradientStop {
color: Qt.rgba(1, 1, 1, 0)
position: 1.0
}
}
}
}
IconButton {
Layout.fillHeight: true
Layout.margins: Appearance.padding.normal
Layout.preferredWidth: height
icon: "delete"
inactiveColor: Qt.alpha(DynamicColors.palette.m3error, 0.8)
inactiveOnColor: DynamicColors.palette.m3onError
isToggle: false
onClicked: ClipHistory.deleteEntry(clipItem.modelData)
}
}
StateLayer {
id: textLayer
onClicked: ClipHistory.copy(clipItem.modelData)
}
}
highlight: CustomRect {
color: DynamicColors.palette.m3onSurface
implicitHeight: view.currentItem?.height ?? 0
implicitWidth: view.width
opacity: 0.08
radius: Appearance.rounding.small
y: view.currentItem?.y ?? 0
Behavior on y {
Anim {
duration: Appearance.anim.durations.small
easing.bezierCurve: Appearance.anim.curves.expressiveEffects
}
}
}
model: ScriptModel {
values: ClipHistory.fuzzyQuery(searchField.text)
}
onCurrentItemChanged: {
if (!currentItem)
return;
ClipHistory.currentEntry = currentItem.modelData;
ClipHistory.refreshPreview();
}
onVisibleChanged: currentIndex = 0
CustomClippingWrapperRect {
anchors.fill: parent
child: view.contentItem
radius: Appearance.rounding.small
}
}
}
}
+41
View File
@@ -0,0 +1,41 @@
pragma ComponentBehavior: Bound
import Quickshell
import QtQuick
import qs.Components
import qs.Config
Item {
id: root
property int contentHeight
property real offsetScale: shouldBeActive ? 0 : 1
required property ShellScreen screen
readonly property bool shouldBeActive: visibilities.clipboard && Config.clipboard.enabled
required property PersistentProperties visibilities
anchors.bottomMargin: (-implicitHeight - 5) * offsetScale
implicitHeight: content.implicitHeight + Appearance.padding.normal * 2
implicitWidth: content.implicitWidth + Appearance.padding.normal * 2 || 400
opacity: 1 - offsetScale
visible: offsetScale < 1
Behavior on offsetScale {
Anim {
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
}
Loader {
id: content
active: root.shouldBeActive || root.visible
anchors.centerIn: parent
sourceComponent: Content {
screen: root.screen
visibilities: root.visibilities
}
}
}
+1
View File
@@ -34,6 +34,7 @@ CustomRect {
StateLayer {
acceptedButtons: Qt.LeftButton
color: root.visibilities.dashboard ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface
onClicked: {
root.visibilities.dashboard = !root.visibilities.dashboard;
+3 -4
View File
@@ -17,8 +17,8 @@ Item {
readonly property Popout currentPopout: content.children.find(c => c.shouldBeActive) ?? null
required property PopoutState popouts
implicitHeight: (currentPopout?.implicitHeight ?? 0) + 5 * 2
implicitWidth: (currentPopout?.implicitWidth ?? 0) + 5 * 2
implicitHeight: (currentPopout?.implicitHeight ?? 0) + Appearance.padding.small * 2
implicitWidth: (currentPopout?.implicitWidth ?? 0) + Appearance.padding.small * 2
Item {
id: content
@@ -28,7 +28,7 @@ Item {
Popout {
name: "audio"
sourceComponent: AudioPopup {
sourceComponent: AudioPopout {
wrapper: root.wrapper
}
}
@@ -73,7 +73,6 @@ Item {
name: "upower"
sourceComponent: UPowerPopout {
wrapper: root.popouts
}
}
+2 -2
View File
@@ -10,9 +10,9 @@ import qs.Components
Item {
id: root
required property PersistentProperties dashState
readonly property real nonAnimHeight: view.implicitHeight + viewWrapper.anchors.margins * 2
readonly property real nonAnimWidth: view.implicitWidth + viewWrapper.anchors.margins * 2
required property PersistentProperties state
required property PersistentProperties visibilities
implicitHeight: nonAnimHeight
@@ -59,7 +59,7 @@ Item {
index: 0
sourceComponent: Dash {
state: root.state
dashState: root.dashState
visibilities: root.visibilities
}
}
+3 -3
View File
@@ -10,9 +10,9 @@ import qs.Modules.Dashboard.Dash
GridLayout {
id: root
required property PersistentProperties dashState
readonly property bool dashboardVisible: visibilities.dashboard
property int radius: Appearance.rounding.smallest
required property PersistentProperties state
required property PersistentProperties visibilities
columnSpacing: Appearance.spacing.smaller
@@ -60,7 +60,7 @@ GridLayout {
User {
id: user
state: root.state
dashState: root.dashState
}
}
@@ -86,7 +86,7 @@ GridLayout {
Calendar {
id: calendar
state: root.state
dashState: root.dashState
}
}
+293 -149
View File
@@ -1,25 +1,50 @@
pragma ComponentBehavior: Bound
import Quickshell
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Components
import qs.Helpers
import qs.Config
import qs.Modules
CustomMouseArea {
id: root
readonly property int currMonth: state.currentDate.getMonth()
readonly property int currYear: state.currentDate.getFullYear()
required property var state
property int activeGrid: 1
required property PersistentProperties dashState
property bool initialized: false
property int month1
property int month2
readonly property int realCurrMonth: dashState.currentDate.getMonth()
readonly property int realCurrYear: dashState.currentDate.getFullYear()
property int year1
property int year2
function handleDateChange() {
const currM = activeGrid === 1 ? month1 : month2;
const currY = activeGrid === 1 ? year1 : year2;
if (realCurrMonth !== currM || realCurrYear !== currY) {
monthChangeAnim.direction = (realCurrYear > currY || (realCurrYear === currY && realCurrMonth > currM)) ? -1 : 1;
if (activeGrid === 1) {
month2 = realCurrMonth;
year2 = realCurrYear;
activeGrid = 2;
} else {
month1 = realCurrMonth;
year1 = realCurrYear;
activeGrid = 1;
}
if (initialized)
monthChangeAnim.restart();
}
}
function onWheel(event: WheelEvent): void {
if (event.angleDelta.y > 0)
root.state.currentDate = new Date(root.currYear, root.currMonth - 1, 1);
root.dashState.currentDate = new Date(root.realCurrYear, root.realCurrMonth - 1, 1);
else if (event.angleDelta.y < 0)
root.state.currentDate = new Date(root.currYear, root.currMonth + 1, 1);
root.dashState.currentDate = new Date(root.realCurrYear, root.realCurrMonth + 1, 1);
}
acceptedButtons: Qt.MiddleButton
@@ -27,102 +52,160 @@ CustomMouseArea {
anchors.right: parent.right
implicitHeight: inner.implicitHeight + inner.anchors.margins * 2
onClicked: root.state.currentDate = new Date()
Component.onCompleted: {
month1 = realCurrMonth;
year1 = realCurrYear;
month2 = realCurrMonth;
year2 = realCurrYear;
initialized = true;
}
onClicked: root.dashState.currentDate = new Date()
onRealCurrMonthChanged: handleDateChange()
onRealCurrYearChanged: handleDateChange()
SequentialAnimation {
id: monthChangeAnim
property int direction: 0
ScriptAction {
script: {
if (activeGrid === 1) {
titleTranslate1.x = -monthChangeAnim.direction * titleClip.width;
grid1Translate.x = -monthChangeAnim.direction * gridClip.width;
titleTranslate2.x = 0;
grid2Translate.x = 0;
} else {
titleTranslate2.x = -monthChangeAnim.direction * titleClip.width;
grid2Translate.x = -monthChangeAnim.direction * gridClip.width;
titleTranslate1.x = 0;
grid1Translate.x = 0;
}
}
}
ParallelAnimation {
Anim {
property: "x"
target: titleTranslate1
to: root.activeGrid === 1 ? 0 : monthChangeAnim.direction * titleClip.width
type: Anim.DefaultSpatial
}
Anim {
property: "x"
target: grid1Translate
to: root.activeGrid === 1 ? 0 : monthChangeAnim.direction * gridClip.width
type: Anim.DefaultSpatial
}
Anim {
property: "x"
target: titleTranslate2
to: root.activeGrid === 2 ? 0 : monthChangeAnim.direction * titleClip.width
type: Anim.DefaultSpatial
}
Anim {
property: "x"
target: grid2Translate
to: root.activeGrid === 2 ? 0 : monthChangeAnim.direction * gridClip.width
type: Anim.DefaultSpatial
}
}
}
ColumnLayout {
id: inner
anchors.fill: parent
anchors.margins: Appearance.padding.large
spacing: Appearance.spacing.small
spacing: Appearance.spacing.extraSmall
RowLayout {
id: monthNavigationRow
Layout.fillWidth: true
spacing: Appearance.spacing.small
spacing: Appearance.spacing.extraSmall
Item {
implicitHeight: prevMonthText.implicitHeight + Appearance.padding.small * 2
implicitWidth: implicitHeight
IconButton {
icon: "chevron_left"
padding: Appearance.padding.small
type: IconButton.Text
StateLayer {
id: prevMonthStateLayer
function onClicked(): void {
root.state.currentDate = new Date(root.currYear, root.currMonth - 1, 1);
}
radius: Appearance.rounding.full
}
MaterialIcon {
id: prevMonthText
anchors.centerIn: parent
color: DynamicColors.palette.m3tertiary
font.pointSize: Appearance.font.size.normal
font.weight: 700
text: "chevron_left"
}
onClicked: root.dashState.currentDate = new Date(root.realCurrYear, root.realCurrMonth - 1, 1)
}
Item {
Layout.fillHeight: true
Layout.fillWidth: true
implicitHeight: monthYearDisplay.implicitHeight + Appearance.padding.small * 2
implicitWidth: monthYearDisplay.implicitWidth + Appearance.padding.small * 2
implicitHeight: monthYearDisplay1.implicitHeight + Appearance.padding.extraSmall * 2
implicitWidth: monthYearDisplay1.implicitWidth + Appearance.padding.large * 2
StateLayer {
function onClicked(): void {
root.state.currentDate = new Date();
color: DynamicColors.palette.m3primary
enabled: {
const now = new Date();
return root.realCurrMonth !== now.getMonth() || root.realCurrYear !== now.getFullYear();
}
radius: pressed ? Appearance.rounding.small : Appearance.rounding.large
Behavior on radius {
Anim {
type: Anim.DefaultEffects
}
}
anchors.fill: monthYearDisplay
anchors.leftMargin: -Appearance.padding.normal
anchors.margins: -Appearance.padding.small
anchors.rightMargin: -Appearance.padding.normal
disabled: {
const now = new Date();
return root.currMonth === now.getMonth() && root.currYear === now.getFullYear();
}
radius: Appearance.rounding.full
onClicked: root.dashState.currentDate = new Date()
}
CustomText {
id: monthYearDisplay
Item {
id: titleClip
anchors.centerIn: parent
color: DynamicColors.palette.m3primary
font.capitalization: Font.Capitalize
font.pointSize: Appearance.font.size.normal
font.weight: 500
text: grid.title
anchors.fill: parent
clip: true
CustomText {
id: monthYearDisplay1
anchors.centerIn: parent
// qmllint enable missing-property
color: DynamicColors.palette.m3primary
// qmllint disable missing-property
text: grid1.item ? grid1.item.title : ""
visible: root.activeGrid === 1 || monthChangeAnim.running
transform: Translate {
id: titleTranslate1
}
}
CustomText {
id: monthYearDisplay2
anchors.centerIn: parent
// qmllint enable missing-property
color: DynamicColors.palette.m3primary
// qmllint disable missing-property
text: grid2.item ? grid2.item.title : ""
visible: root.activeGrid === 2 || monthChangeAnim.running
transform: Translate {
id: titleTranslate2
}
}
}
}
Item {
implicitHeight: nextMonthText.implicitHeight + Appearance.padding.small * 2
implicitWidth: implicitHeight
IconButton {
icon: "chevron_right"
padding: Appearance.padding.small
type: IconButton.Text
StateLayer {
id: nextMonthStateLayer
function onClicked(): void {
root.state.currentDate = new Date(root.currYear, root.currMonth + 1, 1);
}
radius: Appearance.rounding.full
}
MaterialIcon {
id: nextMonthText
anchors.centerIn: parent
color: DynamicColors.palette.m3tertiary
font.pointSize: Appearance.font.size.normal
font.weight: 700
text: "chevron_right"
}
onClicked: root.dashState.currentDate = new Date(root.realCurrYear, root.realCurrMonth + 1, 1)
}
}
@@ -130,109 +213,170 @@ CustomMouseArea {
id: daysRow
Layout.fillWidth: true
locale: grid.locale
locale: Qt.locale()
delegate: CustomText {
required property var model
color: (model.day === 0) ? DynamicColors.palette.m3secondary : DynamicColors.palette.m3onSurfaceVariant
font.weight: 500
color: (model.day === 0) ? DynamicColors.palette.m3tertiary : DynamicColors.palette.m3onSurface
horizontalAlignment: Text.AlignHCenter
text: model.shortName
text: Qt.locale("en_US").standaloneDayName(model.day, Locale.ShortFormat)
}
}
Item {
id: gridClip
Layout.fillWidth: true
implicitHeight: grid.implicitHeight
clip: true
implicitHeight: grid1.implicitHeight
MonthGrid {
id: grid
Component {
id: gridComp
anchors.fill: parent
locale: Qt.locale("en_SE")
month: root.currMonth
spacing: 3
year: root.currYear
Item {
id: internalGridContainer
delegate: Item {
id: dayItem
property int month
property alias title: internalGrid.title
property int year
required property var model
implicitHeight: internalGrid.implicitHeight
implicitHeight: text.implicitHeight + Appearance.padding.small * 2
implicitWidth: implicitHeight
MonthGrid {
id: internalGrid
CustomText {
id: text
anchors.fill: parent
locale: daysRow.locale
month: internalGridContainer.month
spacing: 3
title: `${Qt.locale("en_US").standaloneMonthName(month, Locale.LongFormat)} ${year}`
year: internalGridContainer.year
anchors.centerIn: parent
color: {
const dayOfWeek = dayItem.model.date.getUTCDay();
if (dayOfWeek === 6)
return DynamicColors.palette.m3secondary;
delegate: Item {
id: dayItem
return DynamicColors.palette.m3onSurfaceVariant;
required property var model
implicitHeight: text.implicitHeight + Appearance.padding.normal * 2
implicitWidth: implicitHeight
CustomText {
id: text
anchors.centerIn: parent
color: {
const dayOfWeek = dayItem.model.date.getDay();
if (dayOfWeek === 0)
return DynamicColors.palette.m3tertiary;
return DynamicColors.palette.m3onSurfaceVariant;
}
horizontalAlignment: Text.AlignHCenter
opacity: dayItem.model.today || dayItem.model.month === internalGrid.month ? 1 : 0.4
text: internalGrid.locale.toString(dayItem.model.day)
}
}
}
CustomRect {
id: todayIndicator
property Item today
readonly property Item todayItem: internalGrid.contentItem.children.find(c => c.model.today) ?? null
clip: true
color: DynamicColors.palette.m3primary
implicitHeight: width
implicitWidth: today ? (Math.max(today.implicitWidth, today.implicitHeight) - Appearance.padding.extraSmall) : 0
opacity: todayItem ? 1 : 0
radius: Appearance.rounding.full
scale: todayItem ? 1 : 0.7
x: today ? today.x + (today.width - implicitWidth) / 2 : 0
y: today ? today.y + Appearance.padding.extraSmall / 2 : 0
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
Behavior on scale {
Anim {
type: Anim.FastSpatial
}
}
Behavior on x {
Anim {
}
}
Behavior on y {
Anim {
}
}
onTodayItemChanged: {
if (todayItem)
today = todayItem;
}
Coloriser {
colorizationColor: DynamicColors.palette.m3onPrimary
implicitHeight: internalGrid.height
implicitWidth: internalGrid.width
source: internalGrid
sourceColor: DynamicColors.palette.m3onSurface
x: -todayIndicator.x
y: -todayIndicator.y
}
font.pointSize: Appearance.font.size.normal
font.weight: 500
horizontalAlignment: Text.AlignHCenter
opacity: dayItem.model.today || dayItem.model.month === grid.month ? 1 : 0.4
text: grid.locale.toString(dayItem.model.day)
}
}
}
CustomRect {
id: todayIndicator
Loader {
id: grid1
property Item today
readonly property Item todayItem: grid.contentItem.children.find(c => c.model.today) ?? null
anchors.fill: parent
sourceComponent: gridComp
visible: root.activeGrid === 1 || monthChangeAnim.running
clip: true
color: DynamicColors.palette.m3primary
implicitHeight: today?.implicitHeight ?? 0
implicitWidth: today?.implicitWidth ?? 0
opacity: todayItem ? 1 : 0
radius: Appearance.rounding.full
scale: todayItem ? 1 : 0.7
x: today ? today.x + (today.width - implicitWidth) / 2 : 0
y: today?.y ?? 0
Behavior on opacity {
Anim {
}
}
Behavior on scale {
Anim {
}
}
Behavior on x {
Anim {
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
}
Behavior on y {
Anim {
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
transform: Translate {
id: grid1Translate
}
onTodayItemChanged: {
if (todayItem)
today = todayItem;
Binding {
property: "month"
target: grid1.item
value: root.month1
}
Coloriser {
colorizationColor: DynamicColors.palette.m3onPrimary
implicitHeight: grid.height
implicitWidth: grid.width
source: grid
sourceColor: DynamicColors.palette.m3onSurface
x: -todayIndicator.x
y: -todayIndicator.y
Binding {
property: "year"
target: grid1.item
value: root.year1
}
}
Loader {
id: grid2
anchors.fill: parent
sourceComponent: gridComp
visible: root.activeGrid === 2 || monthChangeAnim.running
transform: Translate {
id: grid2Translate
}
Binding {
property: "month"
target: grid2.item
value: root.month2
}
Binding {
property: "year"
target: grid2.item
value: root.year2
}
}
}
+70 -33
View File
@@ -4,6 +4,7 @@ import Quickshell
import QtQuick
import QtQuick.Layouts
import QtQuick.Shapes
import QtQuick.Effects
import ZShell.Services
import qs.Daemons
import qs.Components
@@ -15,7 +16,7 @@ Item {
property real playerProgress: {
const active = Players.active;
return active?.length ? active.position / active.length : 0;
return active?.length ? (active.position % active.length) / active.length : 0;
}
property int rowHeight: Appearance.padding.large + Config.dashboard.sizes.mediaProgressThickness + Appearance.spacing.small
@@ -45,49 +46,85 @@ Item {
Shape {
id: visualizer
readonly property real barW: Math.max(0, (width - gap * (bars - 1)) / bars)
readonly property int bars: Config.services.visualizerBars
property color color: DynamicColors.palette.m3primary
readonly property real gap: Appearance.spacing.small
property color color: DynamicColors.palette.m3tertiary
property color fillColor: Qt.alpha(color, 0.25)
anchors.fill: layout
anchors.leftMargin: -(shape.strokeWidth / 2)
layer.enabled: true
asynchronous: true
data: visualizerBars.instances
preferredRendererType: Shape.CurveRenderer
}
Variants {
id: visualizerBars
model: Array.from({
length: Config.services.visualizerBars
}, (_, i) => i)
ShapePath {
id: visualizerBar
readonly property real magnitude: value * Config.dashboard.sizes.mediaVisualiserSize
required property int modelData
readonly property real value: Math.max(1e-3, Audio.cava.values[modelData])
capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap
startX: (visualizer.barW / 2) + modelData * (visualizer.barW + visualizer.gap)
startY: layout.y + layout.height
id: shape
strokeColor: visualizer.color
strokeWidth: visualizer.barW
fillColor: visualizer.fillColor
strokeWidth: 2
Behavior on strokeColor {
CAnim {
PathSvg {
path: shape.curvePath
}
property string curvePath: {
const values = Audio.cava.values;
const n = values.length;
if (n < 2)
return "";
const h = layout.height + shape.strokeWidth / 2;
const w = layout.width + shape.strokeWidth;
function x(i) {
return i * w / (n - 1);
}
}
PathLine {
relativeX: 0
relativeY: -visualizerBar.magnitude
function y(i) {
return h - values[i] * Config.dashboard.sizes.mediaVisualizerSize;
}
let d = `M 0 ${h} `;
d += `L ${x(0)} ${y(0)} `;
for (let i = 0; i < n - 1; ++i) {
const x0 = x(i);
const y0 = y(i);
const x1 = x(i + 1);
const y1 = y(i + 1);
const prev = Math.max(0, i - 1);
const next = Math.min(n - 1, i + 2);
const c1x = x0 + (x1 - x(prev)) / 6;
const c1y = y0 + (y1 - y(prev)) / 6;
const c2x = x1 - (x(next) - x0) / 6;
const c2y = y1 - (y(next) - y0) / 6;
d += `C ${c1x} ${c1y}, ${c2x} ${c2y}, ${x1} ${y1} `;
}
d += `L ${w} ${h} `;
d += `L 0 ${h} Z`;
return d;
}
}
}
CustomRect {
anchors.fill: visualizer
color: Qt.alpha(DynamicColors.palette.m3shadow, 0.2)
layer.enabled: true
layer.effect: MultiEffect {
maskEnabled: true
maskSource: visualizer
}
}
Shape {
preferredRendererType: Shape.CurveRenderer
@@ -295,12 +332,12 @@ Item {
StateLayer {
id: controlState
function onClicked(): void {
color: control.canUse ? DynamicColors.palette[`m3on${control.set_color}`] : DynamicColors.palette[`m3on${control.set_color}Container`]
enabled: control.canUse
onClicked: {
control.onClicked();
}
color: control.canUse ? DynamicColors.palette[`m3on${control.set_color}`] : DynamicColors.palette[`m3on${control.set_color}Container`]
disabled: !control.canUse
// radius: Appearance.rounding.full
}
+56 -59
View File
@@ -1,92 +1,89 @@
import QtQuick
import QtQuick.Layouts
import ZShell.Services
import qs.Components
import qs.Helpers
import qs.Config
Row {
Item {
id: root
anchors.bottom: parent.bottom
anchors.top: parent.top
padding: Appearance.padding.large
spacing: Appearance.spacing.large
implicitWidth: layout.implicitWidth + layout.anchors.margins * 4
Ref {
service: SystemUsage
ServiceRef {
service: Storage
}
Resource {
color: DynamicColors.palette.m3primary
icon: "memory"
value: SystemUsage.cpuPerc
ServiceRef {
service: Memory
}
Resource {
color: DynamicColors.palette.m3secondary
icon: "memory_alt"
value: SystemUsage.memPerc
ServiceRef {
service: Cpu
}
Resource {
color: DynamicColors.palette.m3tertiary
icon: "gamepad"
value: SystemUsage.gpuPerc
ServiceRef {
service: Gpu
}
Resource {
color: DynamicColors.palette.m3primary
icon: "host"
value: SystemUsage.gpuMemUsed
}
Resource {
color: DynamicColors.palette.m3secondary
icon: "hard_disk"
value: SystemUsage.storagePerc
}
component Resource: Item {
id: res
required property color color
required property string icon
required property real value
ColumnLayout {
id: layout
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.margins: Appearance.padding.large
anchors.top: parent.top
implicitWidth: icon.implicitWidth
spacing: Appearance.spacing.normal
Behavior on value {
Anim {
duration: Appearance.anim.durations.large
}
Resource {
fgColor: DynamicColors.palette.m3primary
icon: "memory"
value: Cpu.percentage
}
CustomRect {
anchors.bottom: icon.top
anchors.bottomMargin: Appearance.spacing.small
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2)
implicitWidth: Config.dashboard.sizes.resourceProgessThickness
radius: Appearance.rounding.full
Resource {
fgColor: DynamicColors.palette.m3secondary
icon: "memory_alt"
value: Memory.percentage
}
CustomRect {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
color: res.color
implicitHeight: res.value * parent.height
radius: Appearance.rounding.full
Resource {
fgColor: DynamicColors.palette.m3tertiary
icon: "gamepad"
value: Gpu.percentage
}
Resource {
fgColor: DynamicColors.palette.m3primary
icon: "host"
value: Gpu.memoryUsed / Gpu.memoryTotal
}
Resource {
fgColor: DynamicColors.palette.m3secondary
icon: "hard_disk"
value: Storage.percentage
}
}
component Resource: CircularProgress {
id: res
required property string icon
Layout.fillHeight: true
implicitSize: height
Behavior on clampedVal {
Anim {
}
}
MaterialIcon {
id: icon
anchors.bottom: parent.bottom
color: res.color
anchors.centerIn: parent
color: res.fgColor
text: res.icon
}
}
+1 -1
View File
@@ -9,7 +9,7 @@ import QtQuick
Row {
id: root
required property PersistentProperties state
required property PersistentProperties dashState
padding: 20
spacing: 12
+1 -1
View File
@@ -32,7 +32,7 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter
sourceComponent: Content {
state: root.dashState
dashState: root.dashState
visibilities: root.visibilities
}
}
+211 -174
View File
@@ -6,227 +6,264 @@ import qs.Components
import qs.Config
Item {
id: contextMenu
id: contextMenu
anchors.fill: parent
z: 999
visible: false
property real menuX: 0
property real menuY: 0
property var targetAppEntry: null
property string targetFilePath: ""
property bool targetIsDir: false
property var targetPaths: []
property string targetFilePath: ""
property bool targetIsDir: false
property var targetAppEntry: null
signal openFileRequested(string path, bool isDir)
signal renameRequested(string path)
property var targetPaths: []
function close() {
visible = false;
}
signal openFileRequested(string path, bool isDir)
signal renameRequested(string path)
function openAt(mouseX, mouseY, path, isDir, appEnt, parentW, parentH, selectionArray) {
targetFilePath = path;
targetIsDir = isDir;
targetAppEntry = appEnt;
property real menuX: 0
property real menuY: 0
targetPaths = (selectionArray && selectionArray.length > 0) ? selectionArray : [path];
CustomClippingRect {
id: popupBackground
readonly property real padding: Appearance.padding.small
menuX = Math.floor(Math.min(mouseX, parentW - popupBackground.implicitWidth));
menuY = Math.floor(Math.min(mouseY, parentH - popupBackground.implicitHeight));
x: contextMenu.menuX
y: contextMenu.menuY
visible = true;
}
color: DynamicColors.tPalette.m3surface
radius: Appearance.rounding.normal
anchors.fill: parent
visible: false
z: 999
implicitWidth: menuLayout.implicitWidth + padding * 2
implicitHeight: menuLayout.implicitHeight + padding * 2
CustomClippingRect {
id: popupBackground
Behavior on opacity { Anim {} }
opacity: contextMenu.visible ? 1 : 0
readonly property real padding: Appearance.padding.small
ColumnLayout {
id: menuLayout
anchors.centerIn: parent
spacing: 0
color: DynamicColors.tPalette.m3surface
implicitHeight: menuLayout.implicitHeight + padding * 2
implicitWidth: menuLayout.implicitWidth + padding * 2
opacity: contextMenu.visible ? 1 : 0
radius: Appearance.rounding.normal
x: contextMenu.menuX
y: contextMenu.menuY
CustomRect {
Layout.preferredWidth: 160
radius: popupBackground.radius - popupBackground.padding
implicitHeight: openRow.implicitHeight + Appearance.padding.small * 2
Behavior on opacity {
Anim {
}
}
RowLayout {
id: openRow
spacing: 8
anchors.fill: parent
anchors.leftMargin: Appearance.padding.smaller
ColumnLayout {
id: menuLayout
MaterialIcon { text: "open_in_new"; font.pointSize: 20 }
CustomText { text: "Open"; Layout.fillWidth: true }
}
anchors.centerIn: parent
spacing: 0
StateLayer {
anchors.fill: parent
CustomRect {
Layout.preferredWidth: 160
implicitHeight: openRow.implicitHeight + Appearance.padding.small * 2
radius: popupBackground.radius - popupBackground.padding
onClicked: {
for (let i = 0; i < contextMenu.targetPaths.length; i++) {
let p = contextMenu.targetPaths[i];
if (p === contextMenu.targetFilePath) {
if (p.endsWith(".desktop") && contextMenu.targetAppEntry) contextMenu.targetAppEntry.execute()
else contextMenu.openFileRequested(p, contextMenu.targetIsDir)
} else {
Quickshell.execDetached(["xdg-open", p])
}
}
contextMenu.close()
}
}
}
RowLayout {
id: openRow
CustomRect {
Layout.fillWidth: true
radius: popupBackground.radius - popupBackground.padding
implicitHeight: openWithRow.implicitHeight + Appearance.padding.small * 2
anchors.fill: parent
anchors.leftMargin: Appearance.padding.smaller
spacing: 8
RowLayout {
id: openWithRow
spacing: 8
anchors.fill: parent
anchors.leftMargin: Appearance.padding.smaller
MaterialIcon {
font.pointSize: 20
text: "open_in_new"
}
MaterialIcon { text: contextMenu.targetIsDir ? "terminal" : "apps"; font.pointSize: 20 }
CustomText { text: contextMenu.targetIsDir ? "Open in terminal" : "Open with..."; Layout.fillWidth: true }
}
CustomText {
Layout.fillWidth: true
text: "Open"
}
}
StateLayer {
anchors.fill: parent
StateLayer {
anchors.fill: parent
onClicked: {
for (let i = 0; i < contextMenu.targetPaths.length; i++) {
let p = contextMenu.targetPaths[i];
if (p === contextMenu.targetFilePath) {
if (p.endsWith(".desktop") && contextMenu.targetAppEntry)
contextMenu.targetAppEntry.execute();
else
contextMenu.openFileRequested(p, contextMenu.targetIsDir);
} else {
Quickshell.execDetached(["xdg-open", p]);
}
}
contextMenu.close();
}
}
}
CustomRect {
Layout.fillWidth: true
implicitHeight: openWithRow.implicitHeight + Appearance.padding.small * 2
radius: popupBackground.radius - popupBackground.padding
RowLayout {
id: openWithRow
anchors.fill: parent
anchors.leftMargin: Appearance.padding.smaller
spacing: 8
MaterialIcon {
font.pointSize: 20
text: contextMenu.targetIsDir ? "terminal" : "apps"
}
CustomText {
Layout.fillWidth: true
text: contextMenu.targetIsDir ? "Open in terminal" : "Open with..."
}
}
StateLayer {
anchors.fill: parent
onClicked: {
if (contextMenu.targetIsDir) {
Quickshell.execDetached([Config.general.apps.terminal, "--working-directory", contextMenu.targetFilePath])
Quickshell.execDetached([Config.general.apps.terminal, "--working-directory", contextMenu.targetFilePath]);
} else {
Quickshell.execDetached(["xdg-open", contextMenu.targetFilePath])
Quickshell.execDetached(["xdg-open", contextMenu.targetFilePath]);
}
contextMenu.close()
contextMenu.close();
}
}
}
}
}
CustomRect {
Layout.fillWidth: true
implicitHeight: 1
color: DynamicColors.palette.m3outlineVariant
Layout.topMargin: 4
Layout.bottomMargin: 4
}
CustomRect {
Layout.bottomMargin: 4
Layout.fillWidth: true
Layout.topMargin: 4
color: DynamicColors.palette.m3outlineVariant
implicitHeight: 1
}
CustomRect {
Layout.fillWidth: true
radius: popupBackground.radius - popupBackground.padding
implicitHeight: copyPathRow.implicitHeight + Appearance.padding.small * 2
CustomRect {
Layout.fillWidth: true
implicitHeight: copyPathRow.implicitHeight + Appearance.padding.small * 2
radius: popupBackground.radius - popupBackground.padding
RowLayout {
id: copyPathRow
spacing: 8
anchors.fill: parent
anchors.leftMargin: Appearance.padding.smaller
RowLayout {
id: copyPathRow
MaterialIcon { text: "content_copy"; font.pointSize: 20 }
CustomText { text: "Copy path"; Layout.fillWidth: true }
}
anchors.fill: parent
anchors.leftMargin: Appearance.padding.smaller
spacing: 8
StateLayer {
anchors.fill: parent
MaterialIcon {
font.pointSize: 20
text: "content_copy"
}
onClicked: {
Quickshell.execDetached(["wl-copy", contextMenu.targetPaths.join("\n")])
contextMenu.close()
}
}
}
CustomText {
Layout.fillWidth: true
text: "Copy path"
}
}
CustomRect {
Layout.fillWidth: true
visible: contextMenu.targetPaths.length === 1
radius: popupBackground.radius - popupBackground.padding
implicitHeight: renameRow.implicitHeight + Appearance.padding.small * 2
StateLayer {
anchors.fill: parent
RowLayout {
id: renameRow
spacing: 8
anchors.fill: parent
anchors.leftMargin: Appearance.padding.smaller
onClicked: {
Quickshell.execDetached(["wl-copy", contextMenu.targetPaths.join("\n")]);
contextMenu.close();
}
}
}
MaterialIcon { text: "edit"; font.pointSize: 20 }
CustomText { text: "Rename"; Layout.fillWidth: true }
}
CustomRect {
Layout.fillWidth: true
implicitHeight: renameRow.implicitHeight + Appearance.padding.small * 2
radius: popupBackground.radius - popupBackground.padding
visible: contextMenu.targetPaths.length === 1
StateLayer {
anchors.fill: parent
RowLayout {
id: renameRow
onClicked: {
contextMenu.renameRequested(contextMenu.targetFilePath)
contextMenu.close()
}
}
}
anchors.fill: parent
anchors.leftMargin: Appearance.padding.smaller
spacing: 8
Rectangle {
Layout.fillWidth: true
implicitHeight: 1
color: DynamicColors.palette.m3outlineVariant
Layout.topMargin: 4
Layout.bottomMargin: 4
}
MaterialIcon {
font.pointSize: 20
text: "edit"
}
CustomRect {
Layout.fillWidth: true
radius: popupBackground.radius - popupBackground.padding
implicitHeight: deleteRow.implicitHeight + Appearance.padding.small * 2
CustomText {
Layout.fillWidth: true
text: "Rename"
}
}
RowLayout {
id: deleteRow
spacing: 8
anchors.fill: parent
anchors.leftMargin: Appearance.padding.smaller
StateLayer {
anchors.fill: parent
MaterialIcon {
text: "delete"
font.pointSize: 20
color: deleteButton.hovered ? DynamicColors.palette.m3onError : DynamicColors.palette.m3error
}
onClicked: {
contextMenu.renameRequested(contextMenu.targetFilePath);
contextMenu.close();
}
}
}
CustomText {
text: "Move to trash"
Layout.fillWidth: true
color: deleteButton.hovered ? DynamicColors.palette.m3onError : DynamicColors.palette.m3error
}
}
Rectangle {
Layout.bottomMargin: 4
Layout.fillWidth: true
Layout.topMargin: 4
color: DynamicColors.palette.m3outlineVariant
implicitHeight: 1
}
StateLayer {
id: deleteButton
anchors.fill: parent
color: DynamicColors.tPalette.m3error
CustomRect {
Layout.fillWidth: true
implicitHeight: deleteRow.implicitHeight + Appearance.padding.small * 2
radius: popupBackground.radius - popupBackground.padding
onClicked: {
let cmd = ["gio", "trash"].concat(contextMenu.targetPaths)
Quickshell.execDetached(cmd)
contextMenu.close()
}
}
}
}
}
RowLayout {
id: deleteRow
function openAt(mouseX, mouseY, path, isDir, appEnt, parentW, parentH, selectionArray) {
targetFilePath = path
targetIsDir = isDir
targetAppEntry = appEnt
anchors.fill: parent
anchors.leftMargin: Appearance.padding.smaller
spacing: 8
targetPaths = (selectionArray && selectionArray.length > 0) ? selectionArray : [path]
MaterialIcon {
color: deleteButton.hovered ? DynamicColors.palette.m3onError : DynamicColors.palette.m3error
font.pointSize: 20
text: "delete"
}
menuX = Math.floor(Math.min(mouseX, parentW - popupBackground.implicitWidth))
menuY = Math.floor(Math.min(mouseY, parentH - popupBackground.implicitHeight))
CustomText {
Layout.fillWidth: true
color: deleteButton.hovered ? DynamicColors.palette.m3onError : DynamicColors.palette.m3error
text: "Move to trash"
}
}
visible = true
}
StateLayer {
id: deleteButton
function close() {
visible = false
}
anchors.fill: parent
color: DynamicColors.tPalette.m3error
onClicked: {
let cmd = ["gio", "trash"].concat(contextMenu.targetPaths);
Quickshell.execDetached(cmd);
contextMenu.close();
}
}
}
}
}
}
+54 -51
View File
@@ -6,16 +6,19 @@ import qs.Components
import qs.Helpers
Item {
id: delegateRoot
id: root
property var appEntry: fileName.endsWith(".desktop") ? DesktopEntries.byId(DesktopUtils.getAppId(fileName)) : null
property bool fileIsDir: model.isDir
property string fileName: model.fileName
property string filePath: model.filePath
property int gridX: model.gridX
property int gridY: model.gridY
required property var contextMenu
property bool fileIsDir: modelData.isDir
property string fileName: modelData.fileName
property string filePath: modelData.filePath
property int gridX: modelData.gridX
property int gridY: modelData.gridY
required property Item iconsRoot
property bool isSnapping: snapAnimX.running || snapAnimY.running
property bool lassoActive
required property var modelData
property string resolvedIcon: {
if (fileName.endsWith(".desktop")) {
if (appEntry && appEntry.icon && appEntry.icon !== "")
@@ -29,8 +32,8 @@ Item {
}
function compensateAndSnap(absVisX, absVisY) {
dragContainer.x = absVisX - delegateRoot.x;
dragContainer.y = absVisY - delegateRoot.y;
dragContainer.x = absVisX - root.x;
dragContainer.y = absVisY - root.y;
snapAnimX.start();
snapAnimY.start();
}
@@ -43,19 +46,19 @@ Item {
return dragContainer.y;
}
height: root.cellHeight
width: root.cellWidth
x: gridX * root.cellWidth
y: gridY * root.cellHeight
height: root.iconsRoot.cellHeight
width: root.iconsRoot.cellWidth
x: gridX * root.iconsRoot.cellWidth
y: gridY * root.iconsRoot.cellHeight
Behavior on x {
enabled: !mouseArea.drag.active && !isSnapping && !root.selectedIcons.includes(filePath)
enabled: !mouseArea.drag.active && !root.isSnapping && !root.iconsRoot.selectedIcons.includes(root.filePath)
Anim {
}
}
Behavior on y {
enabled: !mouseArea.drag.active && !isSnapping && !root.selectedIcons.includes(filePath)
enabled: !mouseArea.drag.active && !root.isSnapping && !root.iconsRoot.selectedIcons.includes(root.filePath)
Anim {
}
@@ -78,8 +81,8 @@ Item {
}
}
transform: Translate {
x: (root.selectedIcons.includes(filePath) && root.dragLeader !== "" && root.dragLeader !== filePath) ? root.groupDragX : 0
y: (root.selectedIcons.includes(filePath) && root.dragLeader !== "" && root.dragLeader !== filePath) ? root.groupDragY : 0
x: (root.iconsRoot.selectedIcons.includes(root.filePath) && root.iconsRoot.dragLeader !== "" && root.iconsRoot.dragLeader !== root.filePath) ? root.iconsRoot.groupDragX : 0
y: (root.iconsRoot.selectedIcons.includes(root.filePath) && root.iconsRoot.dragLeader !== "" && root.iconsRoot.dragLeader !== root.filePath) ? root.iconsRoot.groupDragY : 0
}
transitions: Transition {
Anim {
@@ -88,14 +91,14 @@ Item {
onXChanged: {
if (mouseArea.drag.active) {
root.dragLeader = filePath;
root.groupDragX = x;
root.iconsRoot.dragLeader = root.filePath;
root.iconsRoot.groupDragX = x;
}
}
onYChanged: {
if (mouseArea.drag.active) {
root.dragLeader = filePath;
root.groupDragY = y;
root.iconsRoot.dragLeader = root.filePath;
root.iconsRoot.groupDragY = y;
}
}
@@ -127,10 +130,10 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter
implicitSize: 48
source: {
if (delegateRoot.resolvedIcon.startsWith("file://") || delegateRoot.resolvedIcon.startsWith("/")) {
return delegateRoot.resolvedIcon;
if (root.resolvedIcon.startsWith("file://") || root.resolvedIcon.startsWith("/")) {
return root.resolvedIcon;
} else {
return Quickshell.iconPath(delegateRoot.resolvedIcon, fileIsDir ? "folder" : "text-x-generic");
return Quickshell.iconPath(root.resolvedIcon, root.fileIsDir ? "folder" : "text-x-generic");
}
}
}
@@ -147,7 +150,7 @@ Item {
maximumLineCount: 2
style: Text.Outline
styleColor: "black"
text: (appEntry && appEntry.name !== "") ? appEntry.name : fileName
text: (root.appEntry && root.appEntry.name !== "") ? root.appEntry.name : root.fileName
visible: !renameLoader.active
wrapMode: Text.Wrap
}
@@ -155,7 +158,7 @@ Item {
Loader {
id: renameLoader
active: root.editingFilePath === filePath
active: root.iconsRoot.editingFilePath === root.filePath
anchors.centerIn: parent
height: 24
width: 110
@@ -165,7 +168,7 @@ Item {
anchors.margins: 2
color: "white"
horizontalAlignment: Text.AlignHCenter
text: fileName
text: root.fileName
wrapMode: Text.Wrap
Component.onCompleted: {
@@ -174,22 +177,22 @@ Item {
}
Keys.onPressed: function (event) {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
if (text.trim() !== "" && text !== fileName) {
if (text.trim() !== "" && text !== root.fileName) {
let newName = text.trim();
let newPath = filePath.substring(0, filePath.lastIndexOf('/') + 1) + newName;
let newPath = root.filePath.substring(0, root.filePath.lastIndexOf('/') + 1) + newName;
Quickshell.execDetached(["mv", filePath, newPath]);
Quickshell.execDetached(["mv", root.filePath, newPath]);
}
root.editingFilePath = "";
root.iconsRoot.editingFilePath = "";
event.accepted = true;
} else if (event.key === Qt.Key_Escape) {
root.editingFilePath = "";
root.iconsRoot.editingFilePath = "";
event.accepted = true;
}
}
onActiveFocusChanged: {
if (!activeFocus && root.editingFilePath === filePath) {
root.editingFilePath = "";
if (!activeFocus && root.iconsRoot.editingFilePath === root.filePath) {
root.iconsRoot.editingFilePath = "";
}
}
}
@@ -201,7 +204,7 @@ Item {
anchors.fill: parent
anchors.margins: 4
color: "white"
opacity: root.selectedIcons.includes(filePath) ? 0.2 : 0.0
opacity: root.iconsRoot.selectedIcons.includes(root.filePath) ? 0.2 : 0.0
radius: Appearance.rounding.smallest
Behavior on opacity {
@@ -215,45 +218,45 @@ Item {
acceptedButtons: Qt.LeftButton | Qt.RightButton
anchors.fill: parent
cursorShape: root.lassoActive ? undefined : Qt.PointingHandCursor
cursorShape: root.iconsRoot.lassoActive ? undefined : Qt.PointingHandCursor
drag.target: dragContainer
hoverEnabled: true
onClicked: mouse => {
root.forceActiveFocus();
root.iconsRoot.forceActiveFocus();
if (mouse.button === Qt.RightButton) {
if (!root.selectedIcons.includes(filePath)) {
root.selectedIcons = [filePath];
if (!root.iconsRoot.selectedIcons.includes(root.filePath)) {
root.iconsRoot.selectedIcons = [root.filePath];
}
let pos = mapToItem(root, mouse.x, mouse.y);
root.contextMenu.openAt(pos.x, pos.y, filePath, fileIsDir, appEntry, root.width, root.height, root.selectedIcons);
let pos = mapToItem(root.iconsRoot, mouse.x, mouse.y);
root.contextMenu.openAt(pos.x, pos.y, root.filePath, root.fileIsDir, root.appEntry, root.iconsRoot.width, root.iconsRoot.height, root.iconsRoot.selectedIcons);
} else {
root.selectedIcons = [filePath];
root.iconsRoot.selectedIcons = [root.filePath];
root.contextMenu.close();
}
}
onDoubleClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
if (filePath.endsWith(".desktop") && appEntry)
appEntry.execute();
if (root.filePath.endsWith(".desktop") && root.appEntry)
root.appEntry.execute();
else
root.exec(filePath, fileIsDir);
root.iconsRoot.exec(root.filePath, root.fileIsDir);
}
}
onPressed: mouse => {
if (mouse.button === Qt.LeftButton && !root.selectedIcons.includes(filePath)) {
root.selectedIcons = [filePath];
if (mouse.button === Qt.LeftButton && !root.iconsRoot.selectedIcons.includes(root.filePath)) {
root.iconsRoot.selectedIcons = [root.filePath];
}
}
onReleased: {
if (drag.active) {
let absoluteX = delegateRoot.x + dragContainer.x;
let absoluteY = delegateRoot.y + dragContainer.y;
let snapX = Math.max(0, Math.round(absoluteX / root.cellWidth));
let snapY = Math.max(0, Math.round(absoluteY / root.cellHeight));
let absoluteX = root.x + dragContainer.x;
let absoluteY = root.y + dragContainer.y;
let snapX = Math.max(0, Math.round(absoluteX / root.iconsRoot.cellWidth));
let snapY = Math.max(0, Math.round(absoluteY / root.iconsRoot.cellHeight));
root.performMassDrop(filePath, snapX, snapY);
root.iconsRoot.performMassDrop(root.filePath, snapX, snapY);
}
}
+44 -15
View File
@@ -1,11 +1,12 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import qs.Modules
import ZShell.Services
import qs.Helpers
import qs.Config
import qs.Components
import qs.Paths
import ZShell.Services
Item {
id: root
@@ -23,7 +24,33 @@ Item {
property real startY: 0
function exec(filePath, isDir) {
const cmd = ["xdg-open", filePath];
let type = DesktopUtils.getFileType(filePath, isDir);
let cmd = [];
switch (type) {
case "image":
cmd = [Config.general.apps.image, filePath];
break;
case "video":
cmd = [Config.general.apps.playback, filePath];
break;
case "audio":
cmd = [Config.general.apps.audio, filePath];
break;
case "archive":
cmd = [Config.general.apps.archiver, filePath];
break;
case "directory":
cmd = [Config.general.apps.explorer, filePath];
break;
case "text":
cmd = [Config.general.apps.editor, filePath];
break;
case "document":
cmd = [Config.general.apps.document, filePath];
break;
default:
cmd = ["xdg-open", filePath];
}
Quickshell.execDetached(cmd);
}
@@ -57,6 +84,7 @@ Item {
root.groupDragY = 0;
}
anchors.fill: parent
focus: true
Keys.onPressed: event => {
@@ -67,6 +95,8 @@ Item {
DesktopModel {
id: desktopModel
rows: Math.max(1, Math.floor(gridArea.height / root.cellHeight))
Component.onCompleted: loadDirectory(FileUtils.trimFileProtocol(Paths.desktop))
}
@@ -133,10 +163,10 @@ Item {
lasso.width = Math.abs(mouse.x - root.startX);
lasso.height = Math.abs(mouse.y - root.startY);
let minCol = Math.floor((lasso.x - gridArea.x) / cellWidth);
let maxCol = Math.floor((lasso.x + lasso.width - gridArea.x) / cellWidth);
let minRow = Math.floor((lasso.y - gridArea.y) / cellHeight);
let maxRow = Math.floor((lasso.y + lasso.height - gridArea.y) / cellHeight);
let minCol = Math.floor((lasso.x - gridArea.x) / root.cellWidth);
let maxCol = Math.floor((lasso.x + lasso.width - gridArea.x) / root.cellWidth);
let minRow = Math.floor((lasso.y - gridArea.y) / root.cellHeight);
let maxRow = Math.floor((lasso.y + lasso.height - gridArea.y) / root.cellHeight);
let newSelection = [];
for (let i = 0; i < gridArea.children.length; i++) {
@@ -158,10 +188,10 @@ Item {
} else {
bgContextMenu.close();
root.selectedIcons = [];
root.startX = Math.floor(mouse.x);
root.startY = Math.floor(mouse.y);
lasso.x = Math.floor(mouse.x);
lasso.y = Math.floor(mouse.y);
root.startX = mouse.x;
root.startY = mouse.y;
lasso.x = mouse.x;
lasso.y = mouse.y;
lasso.width = 0;
lasso.height = 0;
lasso.showLasso();
@@ -178,15 +208,15 @@ Item {
anchors.fill: parent
anchors.margins: 20
anchors.topMargin: 40
visible: true
Repeater {
model: desktopModel
delegate: DesktopIconDelegate {
property int itemIndex: index
required property int index
lassoActive: root.lassoActive
contextMenu: desktopMenu
iconsRoot: root
}
}
}
@@ -202,6 +232,5 @@ Item {
BackgroundContextMenu {
id: bgContextMenu
}
}

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