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

This commit is contained in:
2026-10-22 23:21:20 +02:00
469 changed files with 40652 additions and 7947 deletions
+5
View File
@@ -0,0 +1,5 @@
CompileFlags:
Remove:
# GCC-only flags from the system's default spec that clang/clangd cannot
# parse. They appear in compile_commands.json but are not set by the project.
- -mno-direct-extern-access
-1
View File
@@ -38,7 +38,6 @@ body:
attributes: attributes:
label: OS label: OS
options: options:
- NixOS
- Arch - Arch
- Debian / Ubuntu - Debian / Ubuntu
- Fedora - Fedora
+1
View File
@@ -30,6 +30,7 @@ body:
- Refactor / style - Refactor / style
- CI / build / tooling - CI / build / tooling
- Documentation - Documentation
- Other (Describe in description box with header "Issue type")
validations: validations:
required: true required: true
+3
View File
@@ -18,3 +18,6 @@ dist/
network-dev/ network-dev/
**/zshell.build/ **/zshell.build/
**/zshell.dist/ **/zshell.dist/
.opencode/
run-agent.sh
AGENTS.md
+1
View File
@@ -1,3 +1,4 @@
.venv/ .venv/
scripts/fzf.js scripts/fzf.js
scripts/fuzzysort.js scripts/fuzzysort.js
scripts/settings-indexer.js
+2 -1
View File
@@ -3,8 +3,9 @@ FunctionsSpacing=true
IndentWidth=4 IndentWidth=4
MaxColumnWidth=-1 MaxColumnWidth=-1
NewlineType=native NewlineType=native
NormalizeOrder=true GroupAttributesTogether=false
ObjectsSpacing=true ObjectsSpacing=true
SemicolonRule=always SemicolonRule=always
SingleLineEmptyObjects=true
SortImports=false SortImports=false
UseTabs=true UseTabs=true
+31 -22
View File
@@ -26,49 +26,51 @@ if(NOT DEFINED VERSION)
endif() endif()
endif() endif()
if(NOT DEFINED GIT_REVISION)
execute_process(COMMAND git rev-parse HEAD
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
OUTPUT_VARIABLE GIT_REVISION
OUTPUT_STRIP_TRAILING_WHITESPACE
)
if("${GIT_REVISION}" STREQUAL "")
message(FATAL_ERROR "GIT_REVISION is not set and failed to get from git")
endif()
endif()
set(VERSION "${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}") set(VERSION "${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}")
project(ZShell VERSION ${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}) project(ZShell VERSION ${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH})
message(STATUS "ZShell version: ${VERSION}") message(STATUS "ZShell version: ${VERSION}")
include(GNUInstallDirs)
set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib") set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib")
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(ENABLE_MODULES "plugin;shell;m3shapes" CACHE STRING "Modules to build/install") set(ENABLE_MODULES "plugin;shell;cli;m3shapes" CACHE STRING "Modules to build/install")
set(INSTALL_LIBDIR "usr/lib/ZShell" CACHE STRING "Library install dir") set(INSTALL_LIBDIR "${CMAKE_INSTALL_LIBDIR}/ZShell" CACHE STRING "Library install dir")
set(INSTALL_QMLDIR "usr/lib/qt6/qml" CACHE STRING "QML install dir") set(INSTALL_QMLDIR "${CMAKE_INSTALL_LIBDIR}/qt6/qml" CACHE STRING "QML install dir")
set(INSTALL_QSCONFDIR "etc/xdg/quickshell/zshell" CACHE STRING "Quickshell config install dir") set(INSTALL_QSCONFDIR "${CMAKE_INSTALL_SYSCONFDIR}/xdg/quickshell/zshell" CACHE STRING "Quickshell config install dir")
set(INSTALL_GREETERCONFDIR "etc/xdg/quickshell/zshell-greeter" CACHE STRING "Quickshell greeter install dir") set(INSTALL_GREETERCONFDIR "${CMAKE_INSTALL_SYSCONFDIR}/xdg/quickshell/zshell-greeter" CACHE STRING "Quickshell greeter install dir")
set(CMAKE_INSTALL_MESSAGE NEVER) set(CMAKE_INSTALL_MESSAGE NEVER)
add_compile_options( add_compile_options(
-Wall -Wextra -Wpedantic -Wshadow -Wconversion -Wall -Wextra -Wpedantic -Wshadow -Wconversion
-Wold-style-cast -Wnull-dereference -Wdouble-promotion -Wnull-dereference -Wdouble-promotion
-Wformat=2 -Wfloat-equal -Woverloaded-virtual -Wformat=2 -Wfloat-equal
-Wsign-conversion -Wredundant-decls -Wswitch -Wsign-conversion -Wredundant-decls -Wswitch
-Wunreachable-code -Wunreachable-code
$<$<COMPILE_LANGUAGE:CXX>:-Wold-style-cast>
$<$<COMPILE_LANGUAGE:CXX>:-Woverloaded-virtual>
) )
if("shell" IN_LIST ENABLE_MODULES) if("cli" IN_LIST ENABLE_MODULES)
# Build settings index
find_package(Python3 COMPONENTS Interpreter REQUIRED)
set(SETTINGS_INDEX_JSON "${CMAKE_BINARY_DIR}/settings-index.json")
execute_process(
COMMAND ${Python3_EXECUTABLE}
"${CMAKE_SOURCE_DIR}/scripts/build-settings-index.py"
"${CMAKE_SOURCE_DIR}/Modules/Settings"
"${SETTINGS_INDEX_JSON}"
RESULT_VARIABLE SETTINGS_INDEX_RESULT
)
if(NOT SETTINGS_INDEX_RESULT EQUAL 0)
message(FATAL_ERROR "Failed to build settings search index")
endif()
# Nuitka compilation # Nuitka compilation
set(ZSHELL_CLI_BUILD_DIR "${CMAKE_BINARY_DIR}/zshell-cli") set(ZSHELL_CLI_BUILD_DIR "${CMAKE_BINARY_DIR}/zshell-cli")
set(ZSHELL_CLI_DIST "${ZSHELL_CLI_BUILD_DIR}/zshell.dist") set(ZSHELL_CLI_DIST "${ZSHELL_CLI_BUILD_DIR}/zshell.dist")
@@ -188,4 +190,11 @@ if("m3shapes" IN_LIST ENABLE_MODULES)
if(TARGET m3shapesplugin) if(TARGET m3shapesplugin)
set_target_properties(m3shapesplugin PROPERTIES INSTALL_RPATH "$ORIGIN") set_target_properties(m3shapesplugin PROPERTIES INSTALL_RPATH "$ORIGIN")
endif() endif()
# Silence warnings that come from m3shapes files we do not manage.
foreach(m3shapes_target m3shapes_core m3shapes_morph m3shapes_shapes m3shapes m3shapesplugin)
if(TARGET ${m3shapes_target})
target_compile_options(${m3shapes_target} PRIVATE -Wno-shadow -Wno-float-equal)
endif()
endforeach()
endif() endif()
+21 -42
View File
@@ -80,49 +80,29 @@ Slider {
property bool moving property bool moving
function update(): void {
animate = !moving;
binding.when = moving;
font.pointSize = moving ? Tokens.font.size.small : Tokens.font.size.larger;
font.family = moving ? Config.appearance.font.family.sans : Config.appearance.font.family.material;
}
anchors.centerIn: parent anchors.centerIn: parent
anchors.verticalCenterOffset: 1
text: moving ? Math.round(root.value * 100) : root.icon
color: Colors.palette.m3inverseOnSurface color: Colors.palette.m3inverseOnSurface
text: root.icon font: moving ? Tokens.font.body.small : Tokens.font.icon.medium
onMovingChanged: anim.restart() Behavior on moving {
SequentialAnimation {
Binding { Anim {
id: binding target: icon
property: "scale"
property: "text" to: 0.3
target: icon duration: Tokens.anim.durations.small / 2
value: Math.round(root.value * root.multiplier) easing: Tokens.anim.standardAccel
when: false }
} PropertyAction {}
Anim {
SequentialAnimation { target: icon
id: anim property: "scale"
to: 1
Anim { duration: Tokens.anim.durations.normal / 2
duration: Tokens.anim.durations.normal / 2 easing: Tokens.anim.standardDecel
easing: Tokens.anim.standardAccel }
property: "scale"
target: icon
to: 0
}
ScriptAction {
script: icon.update()
}
Anim {
duration: Tokens.anim.durations.normal / 2
easing: Tokens.anim.standardDecel
property: "scale"
target: icon
to: 1
} }
} }
} }
@@ -130,7 +110,7 @@ Slider {
} }
Behavior on value { Behavior on value {
Anim { Anim {
duration: Tokens.anim.durations.large type: Anim.StandardLarge
} }
} }
@@ -138,7 +118,6 @@ Slider {
onValueChanged: { onValueChanged: {
if (!initialized) { if (!initialized) {
initialized = true; initialized = true;
oldValue = value;
return; return;
} }
+2 -6
View File
@@ -20,11 +20,7 @@ CustomRect {
property color disabledColor: Qt.alpha(Colors.palette.m3onSurface, 0.1) property color disabledColor: Qt.alpha(Colors.palette.m3onSurface, 0.1)
property color disabledOnColor: Qt.alpha(Colors.palette.m3onSurface, 0.38) property color disabledOnColor: Qt.alpha(Colors.palette.m3onSurface, 0.38)
property bool fillWidth property bool fillWidth
property font font: ({ property font font: Tokens.font.body.small
family: Config.appearance.font.family.sans,
pointSize: Tokens.font.size.larger,
bold: false
})
property real horizontalPadding: padding property real horizontalPadding: padding
readonly property alias hovered: stateLayer.containsMouse readonly property alias hovered: stateLayer.containsMouse
required implicitHeight required implicitHeight
@@ -55,7 +51,7 @@ CustomRect {
if (internalChecked) if (internalChecked)
return checkedRadius; return checkedRadius;
if (isRound) if (isRound)
return (height || implicitHeight) / 2 * Math.min(1, Tokens.rounding.scale); return implicitHeight / 2 * Math.min(1, Tokens.rounding.scale);
return defaultRadius; return defaultRadius;
} }
+1 -1
View File
@@ -20,7 +20,7 @@ BusyIndicator {
property int animState property int animState
property color bgColor: Colors.palette.m3secondaryContainer property color bgColor: Colors.palette.m3secondaryContainer
property color fgColor: Colors.palette.m3primary property color fgColor: Colors.palette.m3primary
property real implicitSize: Tokens.font.size.normal * 3 property real implicitSize: Tokens.font.body.medium.pointSize * 3
property real internalStrokeWidth: strokeWidth property real internalStrokeWidth: strokeWidth
readonly property alias progress: manager.progress readonly property alias progress: manager.progress
property real strokeWidth: Tokens.padding.extraSmall * 0.8 property real strokeWidth: Tokens.padding.extraSmall * 0.8
+5 -5
View File
@@ -46,10 +46,10 @@ Item {
} }
function syncFromPenColor() { function syncFromPenColor() {
if (!drawing.drawingState) if (!drawing.screenState)
return; return;
const c = drawing.drawingState.penColor; const c = drawing.screenState.penColor;
if (c.hsvSaturation > 0) { if (c.hsvSaturation > 0) {
currentHue = c.hsvHue; currentHue = c.hsvHue;
@@ -86,7 +86,7 @@ Item {
currentHue = relative / arcSweep; currentHue = relative / arcSweep;
lastChromaticHue = currentHue; lastChromaticHue = currentHue;
drawing.drawingState.penColor = Qt.hsva(currentHue, drawing.drawingState.penColor.hsvSaturation, drawing.drawingState.penColor.hsvValue, drawing.drawingState.penColor.a); drawing.screenState.penColor = Qt.hsva(currentHue, drawing.screenState.penColor.hsvSaturation, drawing.screenState.penColor.hsvValue, drawing.screenState.penColor.a);
} }
implicitHeight: 180 implicitHeight: 180
@@ -112,7 +112,7 @@ Item {
root.syncFromPenColor(); root.syncFromPenColor();
} }
target: root.drawing.drawingState target: root.drawing.screenState
} }
Canvas { Canvas {
@@ -152,7 +152,7 @@ Item {
CustomRect { CustomRect {
anchors.centerIn: parent anchors.centerIn: parent
color: root.drawing?.drawingState.penColor color: root.drawing?.screenState.penColor
implicitHeight: implicitWidth implicitHeight: implicitWidth
implicitWidth: canvas.height - root.handleSize - Tokens.padding.extraLarge * 2 implicitWidth: canvas.height - root.handleSize - Tokens.padding.extraLarge * 2
radius: Tokens.rounding.full radius: Tokens.rounding.full
+6 -3
View File
@@ -1,12 +1,12 @@
import QtQuick import QtQuick
import qs.Helpers import qs.Services
Flickable { Flickable {
id: root id: root
property bool doneFakeFlick property bool doneFakeFlick
interactive: !Visibilities.getForActive().isDrawing interactive: !ShellState.forActive().isDrawing
maximumFlickVelocity: 3000 maximumFlickVelocity: 3000
rebound: Transition { rebound: Transition {
@@ -28,6 +28,9 @@ Flickable {
interval: 10 interval: 10
running: root.doneFakeFlick running: root.doneFakeFlick
onTriggered: root.doneFakeFlick = false onTriggered: {
root.doneFakeFlick = false;
root.returnToBounds();
}
} }
} }
+19 -16
View File
@@ -1,33 +1,36 @@
import QtQuick import QtQuick
import qs.Helpers import qs.Services
ListView { ListView {
id: root id: root
property bool doneFakeFlick property bool doneFakeFlick
interactive: !Visibilities.getForActive()?.isDrawing interactive: !ShellState.forActive()?.isDrawing
maximumFlickVelocity: 3000 maximumFlickVelocity: 3000
rebound: Transition { rebound: Transition {
onRunningChanged: { // onRunningChanged: {
if (!running && !root.doneFakeFlick) { // if (!running && !root.doneFakeFlick) {
root.doneFakeFlick = true; // root.doneFakeFlick = true;
root.flick(1, 1); // root.flick(1, 1);
root.flick(-1, -1); // root.flick(-1, -1);
Qt.callLater(() => root.cancelFlick()); // Qt.callLater(() => root.cancelFlick());
} // }
} // }
Anim { Anim {
properties: "x,y" properties: "x,y"
type: Anim.DefaultEffects
} }
} }
Timer { // Timer {
interval: 10 // interval: 10
running: root.doneFakeFlick // running: root.doneFakeFlick
//
onTriggered: root.doneFakeFlick = false // onTriggered: {
} // root.doneFakeFlick = false;
// }
// }
} }
+1
View File
@@ -4,6 +4,7 @@ MouseArea {
property int scrollAccumulatedY: 0 property int scrollAccumulatedY: 0
function onWheel(event: WheelEvent): void { function onWheel(event: WheelEvent): void {
event.accepted = false;
} }
onWheel: event => { onWheel: event => {
+3 -4
View File
@@ -6,7 +6,7 @@ import qs.Services
RadioButton { RadioButton {
id: root id: root
font.pointSize: Tokens.font.size.normal font: Tokens.font.body.medium
implicitHeight: Math.max(implicitIndicatorHeight, implicitContentHeight) implicitHeight: Math.max(implicitIndicatorHeight, implicitContentHeight)
implicitWidth: implicitIndicatorWidth + implicitContentWidth + contentItem.anchors.leftMargin implicitWidth: implicitIndicatorWidth + implicitContentWidth + contentItem.anchors.leftMargin
@@ -14,7 +14,7 @@ RadioButton {
anchors.left: outerCircle.right anchors.left: outerCircle.right
anchors.leftMargin: 10 anchors.leftMargin: 10
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
font.pointSize: root.font.pointSize font: root.font
text: root.text text: root.text
} }
indicator: Rectangle { indicator: Rectangle {
@@ -29,8 +29,7 @@ RadioButton {
radius: Tokens.rounding.full radius: Tokens.rounding.full
Behavior on border.color { Behavior on border.color {
CAnim { CAnim {}
}
} }
StateLayer { StateLayer {
+1 -2
View File
@@ -6,7 +6,6 @@ Rectangle {
color: "transparent" color: "transparent"
Behavior on color { Behavior on color {
CAnim { CAnim {}
}
} }
} }
+69 -81
View File
@@ -1,32 +1,41 @@
pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Templates import QtQuick.Templates
import qs.Helpers
import ZShell.Config
import qs.Services import qs.Services
import ZShell.Config
ScrollBar { ScrollBar {
id: root id: root
required property Flickable flickable
readonly property bool isHorizontal: flickable && flickable.ScrollBar.horizontal === root
readonly property real axisSize: isHorizontal ? flickable.width : flickable.height
readonly property real axisContentSize: isHorizontal ? flickable.contentWidth : flickable.contentHeight
readonly property real axisContentPos: isHorizontal ? (reversed ? flickable.contentX : (flickable.contentX - flickable.originX)) : (reversed ? flickable.contentY : (flickable.contentY - flickable.originY))
readonly property real axisLength: isHorizontal ? root.width : root.height
readonly property real effectiveSize: Math.max(nonAnimHeight, root.minimumSize) readonly property real effectiveSize: Math.max(nonAnimHeight, root.minimumSize)
readonly property real effectiveTravel: Math.max(0, 1 - root.effectiveSize) readonly property real effectiveTravel: Math.max(0, 1 - root.effectiveSize)
required property Flickable flickable readonly property real nonAnimHeight: root.axisSize / root.axisContentSize
readonly property real nonAnimHeight: flickable.height / flickable.contentHeight readonly property real nonAnimY: root.axisContentPos / root.axisContentSize
readonly property real nonAnimY: flickable.contentY / flickable.contentHeight
readonly property real rawTravel: Math.max(0, 1 - root.nonAnimHeight) readonly property real rawTravel: Math.max(0, 1 - root.nonAnimHeight)
readonly property bool reversed: flickable instanceof ListView && flickable.verticalLayoutDirection === ListView.BottomToTop readonly property bool reversed: isHorizontal ? (flickable instanceof ListView && flickable.layoutDirection === Qt.RightToLeft) : (flickable instanceof ListView && flickable.verticalLayoutDirection === ListView.BottomToTop)
property bool shouldBeActive
readonly property real travelScale: root.rawTravel > 0 ? root.effectiveTravel / root.rawTravel : 0 readonly property real travelScale: root.rawTravel > 0 ? root.effectiveTravel / root.rawTravel : 0
enabled: !Visibilities.getForActive().isDrawing property bool shouldBeActive
implicitWidth: Tokens.padding.extraSmall * 2 property bool inverted
contentItem: Item { enabled: !ShellState.forActive().isDrawing
}
implicitWidth: size === 1 ? 0 : isHorizontal ? 0 : Tokens.padding.extraSmall * 2
implicitHeight: size === 1 ? 0 : isHorizontal ? Tokens.padding.extraSmall * 2 : 0
contentItem: Item {}
Behavior on position { Behavior on position {
enabled: !fullMouse.pressed enabled: !fullMouse.pressed
Anim { Anim {}
}
} }
onHoveredChanged: { onHoveredChanged: {
@@ -47,54 +56,27 @@ ScrollBar {
target: root.flickable target: root.flickable
} }
CustomClippingRect { Loader {
anchors.bottom: parent.bottom anchors.fill: parent
anchors.right: parent.right active: root.size < 1
anchors.top: parent.top sourceComponent: root.isHorizontal ? horizontalTrack : verticalTrack
implicitWidth: handle.implicitWidth }
radius: Tokens.rounding.full
CustomRect { Component {
id: handle id: verticalTrack
anchors.right: parent.right VerticalScrollBarTrack {
color: Colors.palette.m3secondary scrollBar: root
implicitHeight: root.height * root.effectiveSize mouseArea: fullMouse
implicitWidth: fullMouse.pressed || fullMouse.containsMouse ? Tokens.padding.extraSmall * 2 : Tokens.padding.extraSmall }
opacity: { }
if (!root.enabled)
return 0;
if (root.size === 1)
return 0;
if (fullMouse.pressed)
return 1;
if (fullMouse.containsMouse)
return 0.8;
if (root.policy === ScrollBar.AlwaysOn || root.shouldBeActive)
return 0.6;
return 0;
}
radius: Tokens.rounding.full
y: root.reversed ? root.height * (1 + root.nonAnimY) * root.travelScale : root.height * root.nonAnimY * root.travelScale
Behavior on implicitWidth { Component {
Anim { id: horizontalTrack
}
}
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
MouseArea { HorizontalScrollBarTrack {
id: mouse scrollBar: root
mouseArea: fullMouse
acceptedButtons: Qt.NoButton
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
}
} }
} }
@@ -111,25 +93,40 @@ ScrollBar {
property real pressOffset: 0 property real pressOffset: 0
function contentYFromThumbTop(thumbTop) { function contentPosFromThumbStart(thumbStart) {
var visualPos = root.effectiveTravel > 0 ? thumbTop / root.travelScale : 0; var visualPos = root.effectiveTravel > 0 ? thumbStart / root.travelScale : 0;
return root.reversed ? (visualPos - 1) * root.axisContentSize : visualPos * root.axisContentSize;
return root.reversed ? (visualPos - 1) * root.flickable.contentHeight : visualPos * root.flickable.contentHeight;
} }
function updateFromEvent(event) { function updateFromEvent(event) {
var posInTrack = event.y / root.height; var eventPos = root.isHorizontal ? event.x : event.y;
var thumbTop = posInTrack - pressOffset; var posInTrack = eventPos / root.axisLength;
thumbTop = Math.max(0, Math.min(root.effectiveTravel, thumbTop)); var thumbStart = posInTrack - pressOffset;
thumbStart = Math.max(0, Math.min(root.effectiveTravel, thumbStart));
root.flickable.contentY = contentYFromThumbTop(thumbTop); var newPos = contentPosFromThumbStart(thumbStart);
if (root.isHorizontal)
root.flickable.contentX = newPos + (root.flickable.originX + (root.reversed ? root.flickable.contentWidth : 0));
else
root.flickable.contentY = newPos + (root.flickable.originY + (root.reversed ? root.flickable.contentHeight : 0));
} }
function visualThumbTop() { function visualThumbStart() {
const visualPos = root.reversed ? (1 + root.nonAnimY) : root.nonAnimY; const visualPos = root.reversed ? (1 + root.nonAnimY) : root.nonAnimY;
return visualPos * root.travelScale; return visualPos * root.travelScale;
} }
function onWheel(event: WheelEvent): void {
if (root.horizontal) {
event.accepted = false;
return;
}
var delta = event.angleDelta.y > 0 ? -0.1 : 0.1;
var newPos = Math.max(0, Math.min(1 - root.size, root.position + (root.inverted ? -delta : delta)));
root.position = newPos;
}
anchors.fill: parent anchors.fill: parent
cursorShape: undefined cursorShape: undefined
hoverEnabled: true hoverEnabled: true
@@ -140,24 +137,15 @@ ScrollBar {
updateFromEvent(event); updateFromEvent(event);
} }
onPressed: event => { onPressed: event => {
var currentTop = visualThumbTop(); var currentStart = visualThumbStart();
var currentBottom = currentTop + root.effectiveSize; var currentEnd = currentStart + root.effectiveSize;
var clickPos = event.y / root.height; var eventPos = root.isHorizontal ? event.x : event.y;
var clickPos = eventPos / root.axisLength;
var clickedInsideThumb = clickPos >= currentTop && clickPos <= currentBottom; var clickedInsideThumb = clickPos >= currentStart && clickPos <= currentEnd;
pressOffset = clickedInsideThumb ? (clickPos - currentStart) : root.effectiveSize / 2;
if (clickedInsideThumb) {
pressOffset = clickPos - currentTop;
} else {
pressOffset = root.effectiveSize / 2;
}
updateFromEvent(event); updateFromEvent(event);
} }
onWheel: event => {
var delta = event.angleDelta.y > 0 ? -0.1 : 0.1;
var newPos = Math.max(0, Math.min(1 - root.size, root.position + delta));
root.position = newPos;
}
} }
} }
+9 -7
View File
@@ -100,15 +100,19 @@ Slider {
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.top: parent.top anchors.top: parent.top
color: root.insetColor color: root.insetColor
font.pixelSize: Tokens.font.size.smaller * 2 fontStyle: {
if (root.height < 96)
return Tokens.font.icon.pixelSize(24).build();
else
return Tokens.font.icon.pixelSize(32).build();
}
text: root.insetIcon text: root.insetIcon
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
visible: !root.wavy && root.insetIcon !== "" visible: !root.wavy && root.insetIcon !== ""
x: Tokens.spacing.extraSmall + dockT * (handle.x + Tokens.spacing.small) x: Tokens.spacing.extraSmall + dockT * (handle.x + Tokens.spacing.small)
Behavior on dockT { Behavior on dockT {
Anim { Anim {}
}
} }
} }
@@ -138,8 +142,7 @@ Slider {
startX: x startX: x
Behavior on color { Behavior on color {
CAnim { CAnim {}
}
} }
Anim on waveProgress { Anim on waveProgress {
duration: root.waveDuration duration: root.waveDuration
@@ -156,8 +159,7 @@ Slider {
Behavior on filledWidth { Behavior on filledWidth {
id: widthBehavior id: widthBehavior
Anim { Anim {}
}
} }
Component.onCompleted: { Component.onCompleted: {
+2 -3
View File
@@ -14,10 +14,9 @@ Text {
property real animateTo: 1 property real animateTo: 1
color: Colors.palette.m3onSurface color: Colors.palette.m3onSurface
font.family: Config.appearance.font.family.sans font: Tokens.font.body.small
font.pointSize: Tokens.font.size.normal
linkColor: Colors.palette.m3onPrimaryFixedVariant linkColor: Colors.palette.m3onPrimaryFixedVariant
renderType: Text.NativeRendering renderType: Text.QtRendering
textFormat: Text.PlainText textFormat: Text.PlainText
Behavior on color { Behavior on color {
+26 -23
View File
@@ -15,29 +15,33 @@ TextFieldBase {
readonly property int clampedRadius: Math.min(horizontalPadding, Math.min(width, height) / 2, radius) readonly property int clampedRadius: Math.min(horizontalPadding, Math.min(width, height) / 2, radius)
readonly property string effectiveSupportingText: isError && errorText ? errorText : supportingText readonly property string effectiveSupportingText: isError && errorText ? errorText : supportingText
readonly property int filledOffset: type === CustomTextField.Filled ? Tokens.spacing.small : 0
readonly property int leadingOffset: leadingIcon ? leadingIconLoader.width + leadingIconLoader.anchors.leftMargin : 0
readonly property real smallFontScale: smallFontSize / font.pointSize
readonly property int supportingTextOffset: effectiveSupportingText ? supportingTextLoader.height + Tokens.spacing.extraSmall : 0
readonly property int trailingOffset: trailingIcon ? trailingIconLoader.width + trailingIconLoader.anchors.rightMargin : 0
readonly property bool valid: !validate || (!text && emptyIsValid) || (validate instanceof RegExp ? validate.test(text) : !!validate(text))
property int horizontalPadding: Tokens.padding.large
property int verticalPadding: Tokens.padding.large
property int radius: Tokens.rounding.small
property string supportingText
property string trailingIcon
property int smallFontSize: Tokens.font.label.small.pointSize
property int type: CustomTextField.Outlined
property var validate // Regex or function
property bool emptyIsValid: true property bool emptyIsValid: true
property string errorText property string errorText
readonly property int filledOffset: type === CustomTextField.Filled ? Tokens.spacing.small : 0
readonly property int horizontalPadding: Tokens.padding.large
property bool isError property bool isError
property string leadingIcon property string leadingIcon
readonly property int leadingOffset: leadingIcon ? leadingIconLoader.width + leadingIconLoader.anchors.leftMargin : 0
property int radius: Tokens.rounding.small
property alias sLayer: stateLayer
readonly property real smallFontScale: smallFontSize / font.pointSize
property int smallFontSize: Tokens.font.size.small
property string supportingText
readonly property int supportingTextOffset: effectiveSupportingText ? supportingTextLoader.height + Tokens.spacing.extraSmall : 0
property string trailingIcon
readonly property int trailingOffset: trailingIcon ? trailingIconLoader.width + trailingIconLoader.anchors.rightMargin : 0
property int type: CustomTextField.Outlined
readonly property bool valid: !validate || (!text && emptyIsValid) || (validate instanceof RegExp ? validate.test(text) : !!validate(text))
property var validate // Regex or function
bottomPadding: Tokens.padding.large + supportingTextOffset - filledOffset bottomPadding: verticalPadding + supportingTextOffset - filledOffset
leftPadding: horizontalPadding + leadingOffset leftPadding: horizontalPadding + leadingOffset
rightPadding: horizontalPadding + trailingOffset rightPadding: horizontalPadding + trailingOffset
topPadding: Tokens.padding.large + filledOffset topPadding: verticalPadding + filledOffset
background: Loader { background: Loader {
anchors.bottomMargin: root.supportingTextOffset anchors.bottomMargin: root.supportingTextOffset
@@ -148,6 +152,7 @@ TextFieldBase {
color: Colors.palette.m3onSurfaceVariant color: Colors.palette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.larger font.pointSize: Tokens.font.size.larger
text: root.leadingIcon text: root.leadingIcon
fontStyle: Tokens.font.icon.builders.medium.scale(0.9).build()
} }
} }
@@ -162,6 +167,7 @@ TextFieldBase {
sourceComponent: MaterialIcon { sourceComponent: MaterialIcon {
color: Colors.palette.m3onSurfaceVariant color: Colors.palette.m3onSurfaceVariant
text: root.trailingIcon text: root.trailingIcon
fontStyle: Tokens.font.icon.builders.medium.scale(0.9).build()
} }
} }
} }
@@ -176,7 +182,7 @@ TextFieldBase {
sourceComponent: CustomText { sourceComponent: CustomText {
color: root.isError ? Colors.palette.m3error : Colors.palette.m3onSurfaceVariant color: root.isError ? Colors.palette.m3error : Colors.palette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.small font: Tokens.font.label.small
text: root.effectiveSupportingText text: root.effectiveSupportingText
} }
} }
@@ -214,12 +220,10 @@ TextFieldBase {
} }
} }
Behavior on strokeColor { Behavior on strokeColor {
CAnim { CAnim {}
}
} }
Behavior on strokeWidth { Behavior on strokeWidth {
Anim { Anim {}
}
} }
PathLine { PathLine {
@@ -294,8 +298,7 @@ TextFieldBase {
implicitHeight: root.activeFocus ? 2 : 1 implicitHeight: root.activeFocus ? 2 : 1
Behavior on implicitHeight { Behavior on implicitHeight {
Anim { Anim {}
}
} }
} }
} }
+42
View File
@@ -0,0 +1,42 @@
import QtQuick
import Quickshell
ShaderEffect {
id: root
property Item target: parent
property real glowSize: 40
property real innerGlow: 0
property real lineWidth: 2
property real intensity: 1.0
property real speed: 1.0
property int period: 6000
property bool running: true
property color color1: "#4285F4"
property color color2: "#9B72CB"
property color color3: "#D96570"
property color color4: "#F2A54A"
readonly property size itemSize: Qt.size(width, height)
readonly property size rectSize: target ? Qt.size(target.width, target.height) : Qt.size(0, 0)
readonly property real radius: (target as Rectangle).radius ?? 0
property real phase: 0
anchors.fill: target
anchors.margins: -glowSize
z: -1
enabled: false
fragmentShader: Qt.resolvedUrl(`${Quickshell.shellDir}/assets/shaders/geminiglow.frag.qsb`)
FrameAnimation {
running: root.running && root.visible
onTriggered: {
const dt = frameTime * 1000;
root.phase = (root.phase + dt * root.speed / root.period) % 1.0;
}
}
}
+56
View File
@@ -0,0 +1,56 @@
import QtQuick
import ZShell.Config
import qs.Services
CustomClippingRect {
id: track
required property var scrollBar
required property var mouseArea
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
implicitHeight: handle.implicitHeight
radius: Tokens.rounding.full
CustomRect {
id: handle
anchors.bottom: parent.bottom
color: Colors.palette.m3secondary
implicitWidth: track.scrollBar.width * track.scrollBar.effectiveSize
implicitHeight: track.mouseArea.pressed || track.mouseArea.containsMouse ? Tokens.padding.extraSmall * 2 : Tokens.padding.extraSmall
opacity: {
if (!track.scrollBar.enabled)
return 0;
if (track.scrollBar.size === 1)
return 0;
if (track.mouseArea.pressed)
return 1;
if (track.mouseArea.containsMouse)
return 0.8;
if (track.scrollBar.policy === CustomScrollBar.AlwaysOn || track.scrollBar.shouldBeActive)
return 0.6;
return 0;
}
radius: Tokens.rounding.full
x: track.scrollBar.reversed ? track.scrollBar.width * (1 + track.scrollBar.nonAnimY) * track.scrollBar.travelScale : track.scrollBar.width * track.scrollBar.nonAnimY * track.scrollBar.travelScale
Behavior on implicitHeight {
Anim {}
}
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
MouseArea {
acceptedButtons: Qt.NoButton
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
}
}
}
+2 -1
View File
@@ -8,6 +8,7 @@ ButtonBase {
property alias icon: label.text property alias icon: label.text
readonly property alias label: label readonly property alias label: label
font: Tokens.font.icon.medium
activeColor: type === IconButton.Filled ? Colors.palette.m3primary : Colors.palette.m3secondary activeColor: type === IconButton.Filled ? Colors.palette.m3primary : Colors.palette.m3secondary
activeOnColor: type === IconButton.Filled ? Colors.palette.m3onPrimary : type === IconButton.Tonal ? Colors.palette.m3onSecondary : Colors.palette.m3primary activeOnColor: type === IconButton.Filled ? Colors.palette.m3onPrimary : type === IconButton.Tonal ? Colors.palette.m3onSecondary : Colors.palette.m3primary
implicitHeight: { implicitHeight: {
@@ -36,7 +37,7 @@ ButtonBase {
anchors.verticalCenterOffset: 1 anchors.verticalCenterOffset: 1
color: root.onColor color: root.onColor
fill: !root.isToggle || root.internalChecked ? 1 : 0 fill: !root.isToggle || root.internalChecked ? 1 : 0
font.pointSize: root.font.pointSize fontStyle: root.font
Behavior on fill { Behavior on fill {
Anim { Anim {
+1 -2
View File
@@ -46,10 +46,9 @@ ButtonBase {
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
color: root.onColor color: root.onColor
fill: root.internalChecked ? 1 : 0 fill: root.internalChecked ? 1 : 0
font: { fontStyle: {
const f = Qt.font(root.font); const f = Qt.font(root.font);
f.pointSize = Math.round(root.font.pointSize * 1.2); f.pointSize = Math.round(root.font.pointSize * 1.2);
f.family = "Material Symbols Rounded";
return f; return f;
} }
+467
View File
@@ -0,0 +1,467 @@
pragma ComponentBehavior: Bound
import QtQuick
Item {
id: root
required property list<var> values
property int xSteps: 24
property real yMin: NaN
property real yMax: NaN
property real yPaddingTop: 0
property real yPaddingBottom: 0
property int verticalLineStep: 0
property int horizontalLineStep: 0
property bool firstXLine: true
property bool lastXLine: false
property int minHorizontalLines: 0
property var xLabel: null
property var yLabel: null
property real smoothing: 0
property bool curveSmoothing: true
property real markerX: -1
property real markerGap: 2 // px gap between line and marker dot edge, each side
property color lineColor: "white"
property color gridColor: "white"
property color markerColor: "white"
property color labelColor: "white"
property real gridAlpha: 0.12
property real labelAlpha: 0.6
property real fillAlpha: 0.15
property real lineWidth: 2
property real markerDotRadius: 4
property real labelFontSize: 10
Canvas {
id: canvas
anchors.fill: parent
renderStrategy: Canvas.Immediate
onWidthChanged: requestPaint()
onHeightChanged: requestPaint()
function strokeCurve(ctx, pts) {
ctx.beginPath();
ctx.moveTo(pts[0].x, pts[0].y);
if (root.curveSmoothing && pts.length > 2) {
const k = 1 / 6;
const n = pts.length;
for (let i = 0; i < n - 1; i++) {
const p0 = pts[Math.max(i - 1, 0)];
const p1 = pts[i];
const p2 = pts[i + 1];
const p3 = pts[Math.min(i + 2, n - 1)];
let c1x = p1.x + (p2.x - p0.x) * k;
let c1y = p1.y + (p2.y - p0.y) * k;
let c2x = p2.x - (p3.x - p1.x) * k;
let c2y = p2.y - (p3.y - p1.y) * k;
const minY = Math.min(p1.y, p2.y);
const maxY = Math.max(p1.y, p2.y);
c1y = Math.max(minY, Math.min(maxY, c1y));
c2y = Math.max(minY, Math.min(maxY, c2y));
ctx.bezierCurveTo(c1x, c1y, c2x, c2y, p2.x, p2.y);
}
} else {
for (let i = 0; i < pts.length - 1; i++) {
const p0 = pts[i];
const p1 = pts[i + 1];
const cx = (p0.x + p1.x) * 0.5;
ctx.bezierCurveTo(cx, p0.y, cx, p1.y, p1.x, p1.y);
}
}
}
// Control points for segment i (pts[i] -> pts[i+1]), matching strokeCurve
function segmentControls(pts, i) {
const p0 = pts[Math.max(i - 1, 0)];
const p1 = pts[i];
const p2 = pts[i + 1];
const p3 = pts[Math.min(i + 2, pts.length - 1)];
if (root.curveSmoothing && pts.length > 2) {
const k = 1 / 6;
let c1x = p1.x + (p2.x - p0.x) * k;
let c1y = p1.y + (p2.y - p0.y) * k;
let c2x = p2.x - (p3.x - p1.x) * k;
let c2y = p2.y - (p3.y - p1.y) * k;
const minY = Math.min(p1.y, p2.y);
const maxY = Math.max(p1.y, p2.y);
c1y = Math.max(minY, Math.min(maxY, c1y));
c2y = Math.max(minY, Math.min(maxY, c2y));
return [c1x, c1y, c2x, c2y];
}
const cx = (p1.x + p2.x) * 0.5;
return [cx, p1.y, cx, p2.y];
}
// Point on the curve at a given x (x is monotonic per segment)
function pointAtX(pts, targetX) {
let j = pts.length - 2;
for (let i = 0; i < pts.length - 1; i++) {
if (targetX <= pts[i + 1].x) {
j = i;
break;
}
}
const p1 = pts[j];
const p2 = pts[j + 1];
const [c1x, c1y, c2x, c2y] = segmentControls(pts, j);
let tlo = 0;
let thi = 1;
for (let it = 0; it < 24; it++) {
const tm = (tlo + thi) / 2;
const u = 1 - tm;
const x = u * u * u * p1.x + 3 * u * u * tm * c1x + 3 * u * tm * tm * c2x + tm * tm * tm * p2.x;
if (x < targetX)
tlo = tm;
else
thi = tm;
}
const t = (tlo + thi) / 2;
const u = 1 - t;
const y = u * u * u * p1.y + 3 * u * u * t * c1y + 3 * u * t * t * c2y + t * t * t * p2.y;
return { x: targetX, y };
}
// Part of the curve left/right of a cut x, including the cut point
function cutLeft(pts, targetX) {
if (targetX <= pts[0].x)
return [];
if (targetX >= pts[pts.length - 1].x)
return pts.slice();
let j = pts.length - 2;
for (let i = 0; i < pts.length - 1; i++) {
if (targetX <= pts[i + 1].x) {
j = i;
break;
}
}
const left = pts.slice(0, j + 1);
if (targetX > pts[j].x)
left.push(pointAtX(pts, targetX));
return left;
}
function cutRight(pts, targetX) {
if (targetX >= pts[pts.length - 1].x)
return [];
if (targetX <= pts[0].x)
return pts.slice();
let j = pts.length - 2;
for (let i = 0; i < pts.length - 1; i++) {
if (targetX <= pts[i + 1].x) {
j = i;
break;
}
}
const right = pts.slice(j + 1);
if (targetX < pts[j + 1].x)
right.unshift(pointAtX(pts, targetX));
return right;
}
onPaint: {
const ctx = getContext("2d");
ctx.reset();
const W = width;
const H = height;
if (W < 2 || H < 2)
return;
const padLeft = root.labelFontSize * 3.4;
const padRight = 2;
const padTop = 2;
const padBottom = root.labelFontSize + 8;
const plot = {
x: padLeft,
y: padTop,
w: W - padLeft - padRight,
h: H - padTop - padBottom
};
if (plot.w < 2 || plot.h < 2)
return;
// Auto-fit: NaN yMin/yMax derive from the data, snapped to the
// horizontal step grid, honoring minHorizontalLines
let lo = root.yMin;
let hi = root.yMax;
const loAuto = Number.isNaN(lo);
const hiAuto = Number.isNaN(hi);
if (loAuto || hiAuto) {
let dmin = Infinity;
let dmax = -Infinity;
for (const v of root.values) {
if (v == null)
continue;
if (v < dmin)
dmin = v;
if (v > dmax)
dmax = v;
}
if (dmin === Infinity) {
dmin = 0;
dmax = 1;
}
const step = root.horizontalLineStep;
const minSteps = root.minHorizontalLines > 1 ? root.minHorizontalLines - 1 : 0;
if (step > 0) {
if (hiAuto) {
const baseLo = loAuto ? Math.floor(dmin / step) * step : lo;
const span = Math.max(Math.ceil((dmax - baseLo) / step), minSteps, 1);
lo = baseLo;
hi = baseLo + span * step;
} else {
const span = Math.max(Math.ceil((hi - dmin) / step), minSteps, 1);
lo = hi - span * step;
}
} else {
if (loAuto)
lo = dmin;
if (hiAuto)
hi = dmax;
if (hi <= lo)
hi = lo + 1;
}
}
const scaleBottom = lo - root.yPaddingBottom;
const scaleTop = hi + root.yPaddingTop;
const range = scaleTop - scaleBottom;
if (range <= 0 || root.xSteps < 1)
return;
let series = root.values;
if (root.smoothing > 0 && series.length > 0) {
const radius = Math.round(root.smoothing);
const raw = series;
series = raw.map((v, i) => {
if (v == null)
return null;
let sum = 0;
let n = 0;
for (let j = -radius; j <= radius; j++) {
const u = raw[i + j];
if (u != null) {
sum += u;
n++;
}
}
return n > 0 ? sum / n : null;
});
}
const xAt = i => plot.x + (i / root.xSteps) * plot.w;
const yAt = v => plot.y + (1 - (v - scaleBottom) / range) * plot.h;
const crisp = v => Math.floor(v) + 0.5;
// 1. Grid lines
ctx.strokeStyle = Qt.alpha(root.gridColor, root.gridAlpha);
ctx.lineWidth = 1;
if (root.verticalLineStep > 0) {
for (let i = 0; i <= root.xSteps; i += root.verticalLineStep) {
if (i === 0 && !root.firstXLine)
continue;
if (i === root.xSteps && !root.lastXLine)
continue;
const x = crisp(xAt(i));
ctx.beginPath();
ctx.moveTo(x, plot.y);
ctx.lineTo(x, H);
ctx.stroke();
}
}
// Lines extend down into yPaddingBottom, snapped to the grid
const vStart = lo - (root.yPaddingBottom > 0 ? Math.floor(root.yPaddingBottom / root.horizontalLineStep) * root.horizontalLineStep : 0);
if (root.horizontalLineStep > 0) {
for (let v = vStart; v <= hi + 0.001; v += root.horizontalLineStep) {
const y = crisp(yAt(v));
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(plot.x + plot.w, y);
ctx.stroke();
}
}
// 2. Labels
ctx.fillStyle = Qt.alpha(root.labelColor, root.labelAlpha);
ctx.font = root.labelFontSize + "px sans-serif";
ctx.textAlign = "left";
ctx.textBaseline = "top";
if (root.verticalLineStep > 0 && root.xLabel) {
for (let i = 0; i <= root.xSteps; i += root.verticalLineStep) {
if (i === 0 && !root.firstXLine)
continue;
if (i === root.xSteps && !root.lastXLine)
continue;
const text = root.xLabel(i);
if (text)
ctx.fillText(text, xAt(i) + 4, plot.y + plot.h + 4);
}
}
ctx.textBaseline = "bottom";
if (root.horizontalLineStep > 0 && root.yLabel) {
for (let v = vStart; v <= hi + 0.001; v += root.horizontalLineStep) {
const text = root.yLabel(v);
if (text)
ctx.fillText(text, 2, yAt(v) - 2);
}
}
// 3. Curve, split into segments at null gaps
const segments = [];
let current = null;
for (let i = 0; i < series.length; i++) {
if (series[i] == null) {
if (current)
segments.push(current);
current = null;
continue;
}
const pt = {
x: xAt(i),
y: yAt(series[i])
};
if (!current)
current = [];
current.push(pt);
}
if (current)
segments.push(current);
ctx.lineCap = "round";
ctx.lineJoin = "round";
const hasMarker = root.markerX >= 0 && root.markerX <= root.xSteps;
const markerPx = hasMarker ? xAt(root.markerX) : 0;
let gapPx = 0;
if (hasMarker) {
// markerGap is measured from the dot edge, perpendicular to the
// curve; project onto x via the local curve slope
let slope = 0;
const seg = segments.find(s => s[0].x <= markerPx && markerPx <= s[s.length - 1].x);
if (seg && seg.length > 2 && markerPx - 1 >= seg[0].x && markerPx + 1 <= seg[seg.length - 1].x)
slope = (pointAtX(seg, markerPx + 1).y - pointAtX(seg, markerPx - 1).y) / 2;
gapPx = (root.markerDotRadius + root.markerGap) / Math.sqrt(1 + slope * slope);
}
for (const seg of segments) {
if (seg.length < 2)
continue;
// Fill stays continuous under the marker
ctx.fillStyle = Qt.alpha(root.lineColor, root.fillAlpha);
strokeCurve(ctx, seg);
ctx.lineTo(seg[seg.length - 1].x, plot.y + plot.h);
ctx.lineTo(seg[0].x, plot.y + plot.h);
ctx.closePath();
ctx.fill();
// Stroke has a gap around the marker
const pieces = hasMarker ? [cutLeft(seg, markerPx - gapPx), cutRight(seg, markerPx + gapPx)] : [seg];
for (const pts of pieces) {
if (pts.length < 2)
continue;
ctx.strokeStyle = root.lineColor;
ctx.lineWidth = root.lineWidth;
strokeCurve(ctx, pts);
ctx.stroke();
}
}
// 4. Marker: dashed vertical + dot
if (root.markerX >= 0 && root.markerX <= root.xSteps) {
const x = markerPx;
ctx.setLineDash([4, 4]);
ctx.strokeStyle = Qt.alpha(root.markerColor, 0.6);
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(crisp(x), plot.y);
ctx.lineTo(crisp(x), H);
ctx.stroke();
ctx.setLineDash([]);
const i0 = Math.floor(root.markerX);
const v0 = series[i0];
const v1 = series[i0 + 1];
let y = null;
if (v0 != null && v1 != null)
y = yAt(v0 + (v1 - v0) * (root.markerX - i0));
else if (v0 != null)
y = yAt(v0);
if (y != null) {
ctx.fillStyle = root.markerColor;
ctx.beginPath();
ctx.arc(x, y, root.markerDotRadius, 0, Math.PI * 2);
ctx.fill();
}
}
}
}
onValuesChanged: canvas.requestPaint()
onXStepsChanged: canvas.requestPaint()
onYMinChanged: canvas.requestPaint()
onYMaxChanged: canvas.requestPaint()
onYPaddingTopChanged: canvas.requestPaint()
onYPaddingBottomChanged: canvas.requestPaint()
onVerticalLineStepChanged: canvas.requestPaint()
onHorizontalLineStepChanged: canvas.requestPaint()
onFirstXLineChanged: canvas.requestPaint()
onLastXLineChanged: canvas.requestPaint()
onMinHorizontalLinesChanged: canvas.requestPaint()
onXLabelChanged: canvas.requestPaint()
onYLabelChanged: canvas.requestPaint()
onSmoothingChanged: canvas.requestPaint()
onCurveSmoothingChanged: canvas.requestPaint()
onMarkerXChanged: canvas.requestPaint()
onMarkerGapChanged: canvas.requestPaint()
onLineColorChanged: canvas.requestPaint()
onGridColorChanged: canvas.requestPaint()
onMarkerColorChanged: canvas.requestPaint()
onLabelColorChanged: canvas.requestPaint()
onGridAlphaChanged: canvas.requestPaint()
onLabelAlphaChanged: canvas.requestPaint()
onFillAlphaChanged: canvas.requestPaint()
onLineWidthChanged: canvas.requestPaint()
onMarkerDotRadiusChanged: canvas.requestPaint()
onLabelFontSizeChanged: canvas.requestPaint()
}
+4 -17
View File
@@ -1,27 +1,14 @@
import QtQuick
import ZShell.Config import ZShell.Config
import qs.Services import qs.Services
CustomText { CustomText {
id: root id: root
readonly property list<int> allowed: [20, 24, 40, 48]
property real fill: 0 property real fill: 0
property int grade: Colors.light ? 0 : -25 property int grade: Colors.light ? 0 : -25
readonly property int opsz: clampOpsz(optical) property font fontStyle: Tokens.font.icon.small
property int optical: fontInfo.pixelSize
function clampOpsz(value): int { renderType: Text.NativeRendering
return allowed.reduce((closest, current) => { font: Tokens.font.icon.size(fontStyle.pointSize).weight(fontStyle.weight).vaxes(fontStyle.variableAxes).fill(fill.toFixed(1)).grade(grade).build()
return Math.abs(current - value) < Math.abs(closest - value) ? current : closest;
});
}
font.family: "Material Symbols Rounded"
font.pointSize: Tokens.font.size.larger
font.variableAxes: ({
FILL: fill,
GRAD: grade,
opsz: opsz,
wght: fontInfo.weight
})
} }
+13 -12
View File
@@ -124,7 +124,7 @@ MouseArea {
model: root.items model: root.items
CustomRect { CustomRect {
id: item id: menuItem
readonly property bool active: modelData === root?.active readonly property bool active: modelData === root?.active
required property int index required property int index
@@ -142,13 +142,13 @@ MouseArea {
} }
StateLayer { StateLayer {
color: item.active ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurface color: menuItem.active ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurface
enabled: root.expanded enabled: root.expanded
onClicked: { onClicked: {
root.itemSelected(item.modelData); root.itemSelected(menuItem.modelData);
root.active = item.modelData; root.active = menuItem.modelData;
item.modelData.clicked(); menuItem.modelData.clicked();
root.expanded = false; root.expanded = false;
} }
} }
@@ -162,26 +162,27 @@ MouseArea {
MaterialIcon { MaterialIcon {
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
color: item.active ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurfaceVariant color: menuItem.active ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurfaceVariant
text: item.modelData?.icon ?? "" text: menuItem.modelData?.icon ?? ""
visible: text !== ""
} }
CustomText { CustomText {
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true Layout.fillWidth: true
color: item.active ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurface color: menuItem.active ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurface
text: item.modelData?.text ?? "" text: menuItem.modelData?.text ?? ""
} }
Loader { Loader {
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
active: item.modelData?.trailingIcon.length > 0 active: menuItem.modelData?.trailingIcon.length > 0
asynchronous: true asynchronous: true
visible: active visible: active
sourceComponent: MaterialIcon { sourceComponent: MaterialIcon {
color: item.active ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurfaceVariant color: menuItem.active ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurfaceVariant
text: item.modelData.trailingIcon text: menuItem.modelData.trailingIcon
} }
} }
} }
+40
View File
@@ -0,0 +1,40 @@
import QtQuick
import Quickshell
PersistentProperties {
required property ShellScreen modelData
reloadableId: "screenState"
// Drawer visibilities
property bool bar
property bool clipboard
property bool dashboard
property bool dock
property bool isDrawing
property bool launcher
property bool osd
property bool resources
property bool settings
property bool sidebar
// Dashboard state
property int dashboardTab: 0
property date dashboardDate: new Date()
// Sidebar state
property int sidebarTab: 0
property list<string> expandedNotifs: []
// Utilities state
property string recordingConfirmDelete
property bool recordingListExpanded
property string recordingMode
// Drawing state
property color penColor: "white"
property int penWidth: 4
// Screenshot state
property bool screenshot
}
+1 -1
View File
@@ -62,7 +62,7 @@ TextFieldBase {
anchors.leftMargin: Tokens.padding.large anchors.leftMargin: Tokens.padding.large
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
color: Colors.palette.m3onSurfaceVariant color: Colors.palette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.large fontStyle: Tokens.font.icon.builders.medium.scale(0.9).build()
text: "search" text: "search"
} }
+3 -4
View File
@@ -2,7 +2,6 @@ import QtQuick
import QtQuick.Shapes import QtQuick.Shapes
import ZShell import ZShell
import ZShell.Config import ZShell.Config
import qs.Helpers
import qs.Services import qs.Services
MouseArea { MouseArea {
@@ -52,7 +51,7 @@ MouseArea {
anchors.fill: parent anchors.fill: parent
cursorShape: !enabled ? undefined : Qt.PointingHandCursor cursorShape: !enabled ? undefined : Qt.PointingHandCursor
enabled: parent.enabled && !Visibilities.getForActive()?.isDrawing enabled: parent.enabled && !ShellState.forActive()?.isDrawing
hoverEnabled: true hoverEnabled: true
Behavior on stateOpacity { Behavior on stateOpacity {
@@ -99,13 +98,13 @@ MouseArea {
id: base id: base
anchors.fill: parent anchors.fill: parent
bottomLeftRadius: root.parent?.bottomLeftRadius ?? radius ?? 0
bottomRightRadius: root.parent?.bottomRightRadius ?? radius ?? 0
color: Colors.palette.m3onSurface color: Colors.palette.m3onSurface
opacity: root.stateOpacity opacity: root.stateOpacity
// Pick up radius from parent if it has one (parent can be anything with radius props) // Pick up radius from parent if it has one (parent can be anything with radius props)
// qmllint disable missing-property // qmllint disable missing-property
radius: root.parent?.radius ?? 0 radius: root.parent?.radius ?? 0
bottomLeftRadius: root.parent?.bottomLeftRadius ?? radius ?? 0
bottomRightRadius: root.parent?.bottomRightRadius ?? radius ?? 0
topLeftRadius: root.parent?.topLeftRadius ?? radius ?? 0 topLeftRadius: root.parent?.topLeftRadius ?? radius ?? 0
topRightRadius: root.parent?.topRightRadius ?? radius ?? 0 topRightRadius: root.parent?.topRightRadius ?? radius ?? 0
// qmllint enable missing-property // qmllint enable missing-property
+86
View File
@@ -0,0 +1,86 @@
import QtQuick
import QtQuick.Templates
import ZShell.Config
import qs.Services
TextArea {
id: root
color: Colors.palette.m3onSurface
cursorVisible: !readOnly
font: Tokens.font.body.small
implicitHeight: contentHeight + topPadding + bottomPadding
implicitWidth: contentWidth + leftPadding + rightPadding
placeholderTextColor: Colors.palette.m3onSurfaceVariant // No anim cause placeholder is custom
renderType: TextArea.NativeRendering
selectedTextColor: color
selectionColor: Qt.alpha(Colors.palette.m3primary, 0.4)
verticalAlignment: TextInput.AlignVCenter
Behavior on color {
CAnim {}
}
Behavior on selectionColor {
CAnim {}
}
cursorDelegate: Item {}
CustomRect {
id: cursor
property bool disableBlink
color: Colors.palette.m3primary
implicitHeight: root.cursorRectangle.height
implicitWidth: 1.5
radius: Tokens.rounding.largeIncreased
x: root.cursorRectangle.x
y: root.cursorRectangle.y
Behavior on x {
Anim {
duration: Tokens.anim.durations.expressiveFastEffects
easing.bezierCurve: [0.2, 1, 0.21, 1, 1, 1]
}
}
Behavior on opacity {
Anim {
type: Anim.StandardSmall
}
}
Connections {
function onCursorPositionChanged(): void {
if (root.activeFocus && root.cursorVisible) {
cursor.opacity = 1;
cursor.disableBlink = true;
enableBlink.restart();
}
}
target: root
}
Timer {
id: enableBlink
interval: 500
onTriggered: cursor.disableBlink = false
}
Timer {
interval: 500
repeat: true
running: root.activeFocus && root.cursorVisible && !cursor.disableBlink
triggeredOnStart: true
onTriggered: parent.opacity = parent.opacity === 1 ? 0 : 1
}
Binding {
cursor.opacity: 0
when: !root.activeFocus || !root.cursorVisible
}
}
}
+96
View File
@@ -0,0 +1,96 @@
import QtQuick
import QtQuick.Templates
import ZShell.Config
import qs.Services
TextEdit {
id: root
property bool animateCursor: true
property alias cursor: cursor
color: Colors.palette.m3onSurface
cursorVisible: !readOnly
font: Tokens.font.body.small
renderType: TextField.NativeRendering
selectedTextColor: color
selectionColor: Qt.alpha(Colors.palette.m3primary, 0.4)
verticalAlignment: TextInput.AlignVCenter
Behavior on color {
CAnim {}
}
cursorDelegate: Item {}
Behavior on selectionColor {
CAnim {}
}
CustomRect {
id: cursor
property bool disableBlink
color: Colors.palette.m3primary
implicitHeight: root.cursorRectangle.height
implicitWidth: 1.5
radius: Tokens.rounding.largeIncreased
x: root.cursorRectangle.x
y: root.cursorRectangle.y
Behavior on opacity {
Anim {
type: Anim.StandardSmall
}
}
Behavior on x {
enabled: root.animateCursor
Anim {
duration: Tokens.anim.durations.expressiveFastEffects
easing.bezierCurve: [0.2, 1, 0.21, 1, 1, 1]
}
}
Behavior on y {
enabled: root.animateCursor
Anim {
duration: Tokens.anim.durations.expressiveFastEffects
easing.bezierCurve: [0.2, 1, 0.21, 1, 1, 1]
}
}
Connections {
function onCursorPositionChanged(): void {
if (root.activeFocus && root.cursorVisible) {
cursor.opacity = 1;
cursor.disableBlink = true;
enableBlink.restart();
}
}
target: root
}
Timer {
id: enableBlink
interval: 500
onTriggered: cursor.disableBlink = false
}
Timer {
interval: 500
repeat: true
running: root.activeFocus && root.cursorVisible && !cursor.disableBlink
triggeredOnStart: true
onTriggered: parent.opacity = parent.opacity === 1 ? 0 : 1
}
Binding {
cursor.opacity: 0
when: !root.activeFocus || !root.cursorVisible
}
}
}
+4 -7
View File
@@ -8,7 +8,7 @@ TextField {
color: Colors.palette.m3onSurface color: Colors.palette.m3onSurface
cursorVisible: !readOnly cursorVisible: !readOnly
font.pointSize: Tokens.font.size.small font: Tokens.font.body.small
implicitHeight: contentHeight + topPadding + bottomPadding implicitHeight: contentHeight + topPadding + bottomPadding
implicitWidth: contentWidth + leftPadding + rightPadding implicitWidth: contentWidth + leftPadding + rightPadding
placeholderTextColor: Colors.palette.m3onSurfaceVariant // No anim cause placeholder is custom placeholderTextColor: Colors.palette.m3onSurfaceVariant // No anim cause placeholder is custom
@@ -18,14 +18,11 @@ TextField {
verticalAlignment: TextInput.AlignVCenter verticalAlignment: TextInput.AlignVCenter
Behavior on color { Behavior on color {
CAnim { CAnim {}
}
}
cursorDelegate: Item {
} }
cursorDelegate: Item {}
Behavior on selectionColor { Behavior on selectionColor {
CAnim { CAnim {}
}
} }
CustomRect { CustomRect {
+3 -4
View File
@@ -36,8 +36,7 @@ CustomRect {
radius: Tokens.rounding.medium radius: Tokens.rounding.medium
Behavior on border.color { Behavior on border.color {
CAnim { CAnim {}
}
} }
Elevation { Elevation {
@@ -84,7 +83,7 @@ CustomRect {
return Colors.palette.m3onError; return Colors.palette.m3onError;
return Colors.palette.m3onSurfaceVariant; return Colors.palette.m3onSurfaceVariant;
} }
font.pointSize: Math.round(Tokens.font.size.large * 1.2) fontStyle: Tokens.font.icon.builders.large.scale(1.2).build()
text: root.modelData.icon text: root.modelData.icon
} }
} }
@@ -107,7 +106,7 @@ CustomRect {
return Colors.palette.m3onSurface; return Colors.palette.m3onSurface;
} }
elide: Text.ElideRight elide: Text.ElideRight
font.pointSize: Tokens.font.size.normal font: Tokens.font.title.small
text: root.modelData.title text: root.modelData.title
} }
+7 -3
View File
@@ -7,7 +7,9 @@ CustomListView {
id: root id: root
property real endFadeOpacity: fadeShouldBeActive(false) ? 0 : 1 property real endFadeOpacity: fadeShouldBeActive(false) ? 0 : 1
property bool fadeEnabled: true
property real fadeAmount: 0.1 property real fadeAmount: 0.1
property real fadeThreshold: 0.0
readonly property bool horizontal: orientation === ListView.Horizontal readonly property bool horizontal: orientation === ListView.Horizontal
property real startFadeOpacity: fadeShouldBeActive(true) ? 0 : 1 property real startFadeOpacity: fadeShouldBeActive(true) ? 0 : 1
@@ -16,7 +18,9 @@ CustomListView {
} }
function fadeShouldBeActive(isStart: bool): bool { function fadeShouldBeActive(isStart: bool): bool {
// When content is smaller than flickable size, hide fade when rebound starts. if (!fadeEnabled)
return false;
if (contentSize() + marginStart() + marginEnd() < viewportSize() && rebound.running && ((isStart ? overshootStart() > 0 : overshootStart() < 0))) { if (contentSize() + marginStart() + marginEnd() < viewportSize() && rebound.running && ((isStart ? overshootStart() > 0 : overshootStart() < 0))) {
return false; return false;
} }
@@ -28,11 +32,11 @@ CustomListView {
} }
function marginEnd(): real { function marginEnd(): real {
return horizontal ? rightMargin : bottomMargin; return horizontal ? rightMargin - fadeThreshold : bottomMargin - fadeThreshold;
} }
function marginStart(): real { function marginStart(): real {
return horizontal ? leftMargin : topMargin; return horizontal ? leftMargin - fadeThreshold : topMargin - fadeThreshold;
} }
function overshootStart(): real { function overshootStart(): real {
+56
View File
@@ -0,0 +1,56 @@
import QtQuick
import ZShell.Config
import qs.Services
CustomClippingRect {
id: track
required property var scrollBar
required property var mouseArea
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.top: parent.top
implicitWidth: handle.implicitWidth
radius: Tokens.rounding.full
CustomRect {
id: handle
anchors.right: parent.right
color: Colors.palette.m3secondary
implicitHeight: track.scrollBar.height * track.scrollBar.effectiveSize
implicitWidth: track.mouseArea.pressed || track.mouseArea.containsMouse ? Tokens.padding.extraSmall * 2 : Tokens.padding.extraSmall
opacity: {
if (!track.scrollBar.enabled)
return 0;
if (track.scrollBar.size === 1)
return 0;
if (track.mouseArea.pressed)
return 1;
if (track.mouseArea.containsMouse)
return 0.8;
if (track.scrollBar.policy === CustomScrollBar.AlwaysOn || track.scrollBar.shouldBeActive)
return 0.6;
return 0;
}
radius: Tokens.rounding.full
y: track.scrollBar.reversed ? track.scrollBar.height * (1 + track.scrollBar.nonAnimY) * track.scrollBar.travelScale : track.scrollBar.height * track.scrollBar.nonAnimY * track.scrollBar.travelScale
Behavior on implicitWidth {
Anim {}
}
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
MouseArea {
acceptedButtons: Qt.NoButton
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
}
}
}
+2 -1
View File
@@ -10,6 +10,7 @@ import ZShell
import qs.Components import qs.Components
import qs.Modules import qs.Modules
import qs.Helpers import qs.Helpers
import qs.Services
import qs.Paths import qs.Paths
import ZShell.Config import ZShell.Config
@@ -25,7 +26,7 @@ Singleton {
property alias server: server property alias server: server
function shouldThrottle(appName: string): bool { function shouldThrottle(appName: string): bool {
if (props.dnd || [...Visibilities.screens.values()].some(v => v.sidebar)) if (props.dnd || ShellState.anySidebarOpen())
return false; return false;
const key = (appName || "unknown").trim().toLowerCase(); const key = (appName || "unknown").trim().toLowerCase();
+5 -10
View File
@@ -3,28 +3,23 @@ pragma ComponentBehavior: Bound
import Quickshell import Quickshell
import QtQuick import QtQuick
import ZShell.Internal import ZShell.Internal
import qs.Components
Item { Item {
id: root id: root
readonly property alias content: contentLoader.item readonly property alias content: contentLoader.item
readonly property PersistentProperties drawingState: PersistentProperties { required property ScreenState screenState
property color penColor: "white"
property int penWidth: 4
reloadableId: "drawingState"
}
required property PersistentProperties visibilities
Loader { Loader {
id: contentLoader id: contentLoader
active: root.visibilities.isDrawing active: root.screenState.isDrawing
anchors.fill: parent anchors.fill: parent
sourceComponent: StrokeCanvas { sourceComponent: StrokeCanvas {
penColor: root.drawingState.penColor penColor: root.screenState.penColor
penWidth: root.drawingState.penWidth penWidth: root.screenState.penWidth
} }
} }
} }
+78 -54
View File
@@ -1,7 +1,7 @@
import Quickshell import Quickshell
import QtQuick import QtQuick
import qs.Components
import ZShell.Config import ZShell.Config
import qs.Components
import qs.Helpers import qs.Helpers
import qs.Modules.Bar.Popouts import qs.Modules.Bar.Popouts
@@ -19,7 +19,7 @@ Item {
required property ShellScreen screen required property ShellScreen screen
property bool singleGestureTriggered: false property bool singleGestureTriggered: false
property bool utilitiesShortcutActive property bool utilitiesShortcutActive
required property PersistentProperties visibilities required property ScreenState screenState
function inBottomPanel(panel: Item, x: real, y: real): bool { function inBottomPanel(panel: Item, x: real, y: real): bool {
const panelHeight = panel.height * (1 - (panel.offsetScale ?? 0)); const panelHeight = panel.height * (1 - (panel.offsetScale ?? 0));
@@ -63,7 +63,7 @@ Item {
cursorShape: (active && centroid.pressPosition.y < root.bar.implicitHeight) ? Qt.ClosedHandCursor : undefined cursorShape: (active && centroid.pressPosition.y < root.bar.implicitHeight) ? Qt.ClosedHandCursor : undefined
dragThreshold: 0 dragThreshold: 0
enabled: !root.visibilities.isDrawing enabled: !root.screenState.isDrawing
grabPermissions: PointerHandler.CanTakeOverFromHandlersOfSameType | PointerHandler.ApprovesTakeOverByAnything grabPermissions: PointerHandler.CanTakeOverFromHandlersOfSameType | PointerHandler.ApprovesTakeOverByAnything
maximumPointCount: 1 maximumPointCount: 1
minimumPointCount: 1 minimumPointCount: 1
@@ -76,45 +76,59 @@ Item {
onCentroidChanged: { onCentroidChanged: {
const x = centroid.position.x; const x = centroid.position.x;
const y = centroid.position.y; const y = centroid.position.y;
const dragX = x - centroid.pressPosition.x; const pressX = centroid.pressPosition.x;
const dragY = y - centroid.pressPosition.y; const pressY = centroid.pressPosition.y;
const dragX = x - pressX;
const dragY = y - pressY;
if (centroid.pressPosition.y >= root.screen.height - Config.bar.border && centroid.pressPosition.x > root.screen.width / 5 && dragY < -Config.launcher.dragThreshold) if (pressY >= root.screen.height - Config.bar.border && pressX > root.screen.width / 5 && dragY < -Config.launcher.dragThreshold)
root.visibilities.launcher = true; root.screenState.launcher = true;
if (root.singleGestureTriggered) if (root.singleGestureTriggered)
return; return;
if (root.geometry.insetTop(root.borderThickness) > centroid.pressPosition.y) { const inDashboard = root.inTopPanel(root.panels.dashboardWrapper, pressX, pressY);
if (dragY > 20) {
root.visibilities.settings = true; if (inDashboard && root.withinPanelWidth(root.panels.dashboardWrapper, x, y)) {
if (dragY > Config.dashboard.dragThreshold) {
root.screenState.dashboard = true;
root.singleGestureTriggered = true; root.singleGestureTriggered = true;
} else if (dragY < -20) { } else if (dragY < -Config.dashboard.dragThreshold) {
root.visibilities.settings = false; root.screenState.dashboard = false;
root.singleGestureTriggered = true; root.singleGestureTriggered = true;
} }
} }
if (centroid.pressPosition.y > root.screen.height - root.geometry.insetBottom(root.borderThickness) && centroid.pressPosition.x < root.screen.width / 5 && dragY < -50) if (root.geometry.insetTop(root.borderThickness) > pressY && (Config.dashboard.showOnHover || !inDashboard)) {
root.visibilities.clipboard = true; if (dragY > 20) {
root.screenState.settings = true;
root.singleGestureTriggered = true;
} else if (dragY < -20) {
root.screenState.settings = false;
root.singleGestureTriggered = true;
}
}
if (!Config.dock.hoverToReveal && centroid.pressPosition.y > root.screen.height - root.geometry.insetTop(root.borderThickness) && centroid.pressPosition.x > root.screen.width / 5 && !root.visibilities.launcher) if (pressY > root.screen.height - root.geometry.insetBottom(root.borderThickness) && pressX < root.screen.width / 5 && dragY < -50)
root.screenState.clipboard = true;
if (!Config.dock.hoverToReveal && pressY > root.screen.height - root.geometry.insetTop(root.borderThickness) && pressX > root.screen.width / 5 && !root.screenState.launcher)
if (dragY < -10) { if (dragY < -10) {
root.visibilities.dock = true; root.screenState.dock = true;
root.singleGestureTriggered = true; root.singleGestureTriggered = true;
} }
if (centroid.pressPosition.x > root.screen.width - Config.bar.border && centroid.pressPosition.y < (root.screen.height / 2) && dragX < -Config.sidebar.dragThreshold) { if (pressX > root.screen.width - Config.bar.border && pressY < (root.screen.height / 2) && dragX < -Config.sidebar.dragThreshold) {
root.visibilities.sidebar = true; root.screenState.sidebar = true;
root.singleGestureTriggered = true; root.singleGestureTriggered = true;
} }
if (centroid.pressPosition.x >= root.screen.width - Config.bar.border && centroid.pressPosition.y > (root.screen.height / 2) && dragX < -20) { if (pressX >= root.screen.width - Config.bar.border && pressY > (root.screen.height / 2) && dragX < -20) {
Hypr.dispatch(`hl.dsp.focus({ workspace = 'r+1', on_current_monitor = true })`); Hypr.dispatch(`hl.dsp.focus({ workspace = 'r+1', on_current_monitor = true })`);
root.singleGestureTriggered = true; root.singleGestureTriggered = true;
} }
if (centroid.pressPosition.x <= Config.bar.border && dragX > 20) { if (pressX <= Config.bar.border && dragX > 20) {
Hypr.dispatch(`hl.dsp.focus({ workspace = 'r-1', on_current_monitor = true })`); Hypr.dispatch(`hl.dsp.focus({ workspace = 'r-1', on_current_monitor = true })`);
root.singleGestureTriggered = true; root.singleGestureTriggered = true;
} }
@@ -127,7 +141,7 @@ Item {
property bool setInitialPoint: false property bool setInitialPoint: false
acceptedButtons: Qt.LeftButton | Qt.RightButton 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) enabled: root.screenState.isDrawing && (!root.inLeftPanel(root.panels.drawing, hoverHandler.point.position.x, hoverHandler.point.position.y) || !root.panels.drawing.expanded)
onActiveChanged: { onActiveChanged: {
if (!active) { if (!active) {
@@ -178,12 +192,12 @@ Item {
HoverHandler { HoverHandler {
id: hoverHandler id: hoverHandler
cursorShape: root.visibilities.isDrawing && !root.inLeftPanel(root.panels.drawing, point.position.x, point.position.y) ? Qt.BlankCursor : undefined cursorShape: root.screenState.isDrawing && !root.inLeftPanel(root.panels.drawing, point.position.x, point.position.y) ? Qt.BlankCursor : undefined
onHoveredChanged: { onHoveredChanged: {
if (!hovered) { if (!hovered) {
if (!root.osdShortcutActive) { if (!root.osdShortcutActive) {
root.visibilities.osd = false; root.screenState.osd = false;
root.panels.osd.hovered = false; root.panels.osd.hovered = false;
} }
@@ -193,13 +207,16 @@ Item {
if (Config.bar.autoHide || root.bar.fullscreen) if (Config.bar.autoHide || root.bar.fullscreen)
root.bar.isHovered = false; root.bar.isHovered = false;
if (root.screenState.dashboard && Config.dashboard.showOnHover)
root.screenState.dashboard = false;
} }
} }
onPointChanged: { onPointChanged: {
const x = point.position.x; const x = point.position.x;
const y = point.position.y; const y = point.position.y;
if (root.visibilities.isDrawing) { if (root.screenState.isDrawing) {
if (root.inLeftPanel(root.panels.drawing, x, y) && !(drawingHandler.point.pressedButtons & Qt.LeftButton)) { if (root.inLeftPanel(root.panels.drawing, x, y) && !(drawingHandler.point.pressedButtons & Qt.LeftButton)) {
root.panels.drawing.expand(); root.panels.drawing.expand();
root.drawing.content.hideHover(); root.drawing.content.hideHover();
@@ -210,7 +227,7 @@ Item {
return; return;
} }
if (!root.visibilities.bar && Config.bar.autoHide && root.geometry.barContains(x, y, true)) if (!root.screenState.bar && Config.bar.autoHide && root.geometry.barContains(x, y, true))
root.bar.isHovered = true; root.bar.isHovered = true;
if (root.bar.fullscreen && !root.bar.isHovered) if (root.bar.fullscreen && !root.bar.isHovered)
@@ -228,7 +245,7 @@ Item {
const showOsd = outOfSidebar && root.inRightPanel(root.panels.osdWrapper, x, y); const showOsd = outOfSidebar && root.inRightPanel(root.panels.osdWrapper, x, y);
if (!root.osdShortcutActive) { if (!root.osdShortcutActive) {
root.visibilities.osd = showOsd; root.screenState.osd = showOsd;
root.panels.osd.hovered = showOsd; root.panels.osd.hovered = showOsd;
} else if (showOsd) { } else if (showOsd) {
root.osdShortcutActive = false; root.osdShortcutActive = false;
@@ -236,8 +253,13 @@ Item {
} }
} }
if (Config.dock.enabled && Config.dock.hoverToReveal && !root.visibilities.dock && !root.visibilities.launcher && root.inBottomPanel(root.panels.dock, x, y)) if (Config.dock.enable && Config.dock.hoverToReveal && !root.screenState.dock && !root.screenState.launcher && root.inBottomPanel(root.panels.dock, x, y))
root.visibilities.dock = true; root.screenState.dock = true;
const inDashboard = root.inTopPanel(root.panels.dashboardWrapper, x, y);
if (Config.dashboard.showOnHover)
root.screenState.dashboard = inDashboard;
if (root.geometry.barContains(x, y)) if (root.geometry.barContains(x, y))
root.bar.checkPopout(root.geometry.axisPos(x, y)); root.bar.checkPopout(root.geometry.axisPos(x, y));
@@ -246,16 +268,17 @@ Item {
Connections { Connections {
function onDashboardChanged() { function onDashboardChanged() {
if (root.visibilities.dashboard) { if (root.screenState.dashboard) {
const inDashboardArea = root.inTopPanel(root.panels.dashboard, root.mouseX, root.mouseY); const inDashboardArea = root.inTopPanel(root.panels.dashboard, root.mouseX, root.mouseY);
if (!inDashboardArea) { if (!inDashboardArea) {
root.dashboardShortcutActive = true; root.dashboardShortcutActive = true;
} }
if (root.panels.launcher.x + root.panels.launcher.width > root.panels.dashboardWrapper.x) if (root.panels.launcher.x + root.panels.launcher.width > root.panels.dashboardWrapper.x)
root.visibilities.launcher = false; root.screenState.launcher = false;
root.visibilities.settings = false; root.screenState.settings = false;
root.visibilities.sidebar = false; root.screenState.sidebar = false;
root.screenState.resources = false;
root.popouts.hasCurrent = false; root.popouts.hasCurrent = false;
} else { } else {
root.dashboardShortcutActive = false; root.dashboardShortcutActive = false;
@@ -263,12 +286,12 @@ Item {
} }
function onIsDrawingChanged() { function onIsDrawingChanged() {
if (!root.visibilities.isDrawing) if (!root.screenState.isDrawing)
root.drawing.clear(); root.drawing.clear();
} }
function onLauncherChanged() { function onLauncherChanged() {
if (!root.visibilities.launcher) { if (!root.screenState.launcher) {
root.dashboardShortcutActive = false; root.dashboardShortcutActive = false;
root.osdShortcutActive = false; root.osdShortcutActive = false;
root.utilitiesShortcutActive = false; root.utilitiesShortcutActive = false;
@@ -276,23 +299,23 @@ Item {
const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY); const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY);
if (!inOsdArea) { if (!inOsdArea) {
root.visibilities.osd = false; root.screenState.osd = false;
root.panels.osd.hovered = false; root.panels.osd.hovered = false;
} }
} }
if (root.visibilities.launcher) { if (root.screenState.launcher) {
if (root.panels.dashboardWrapper.x < root.panels.launcher.x + root.panels.launcher.width) { if (root.panels.dashboardWrapper.x < root.panels.launcher.x + root.panels.launcher.width) {
root.visibilities.dashboard = false; root.screenState.dashboard = false;
} }
root.visibilities.dock = false; root.screenState.dock = false;
root.visibilities.settings = false; root.screenState.settings = false;
} }
} }
function onOsdChanged() { function onOsdChanged() {
if (root.visibilities.osd) { if (root.screenState.osd) {
const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY); const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY);
if (!inOsdArea) { if (!inOsdArea) {
root.osdShortcutActive = true; root.osdShortcutActive = true;
@@ -303,32 +326,33 @@ Item {
} }
function onResourcesChanged() { function onResourcesChanged() {
if (root.visibilities.resources && (root.popouts.currentName.startsWith("audio") || root.popouts.currentName.startsWith("updates"))) { if (root.screenState.resources) {
root.popouts.hasCurrent = false; if (root.popouts.currentName.startsWith("audio") || root.popouts.currentName.startsWith("updates"))
} root.popouts.hasCurrent = false;
if (root.visibilities.resources) root.screenState.settings = false;
root.visibilities.settings = false; root.screenState.dashboard = false;
}
} }
function onSettingsChanged() { function onSettingsChanged() {
if (root.visibilities.settings) { if (root.screenState.settings) {
root.visibilities.resources = false; root.screenState.resources = false;
root.visibilities.dashboard = false; root.screenState.dashboard = false;
root.visibilities.sidebar = false; root.screenState.sidebar = false;
root.panels.popouts.hasCurrent = false; root.panels.popouts.hasCurrent = false;
root.visibilities.launcher = false; root.screenState.launcher = false;
} }
} }
function onSidebarChanged() { function onSidebarChanged() {
if (root.visibilities.sidebar) { if (root.screenState.sidebar) {
root.visibilities.dashboard = false; root.screenState.dashboard = false;
root.visibilities.settings = false; root.screenState.settings = false;
root.popouts.hasCurrent = false; root.popouts.hasCurrent = false;
} }
} }
target: root.visibilities target: root.screenState
} }
} }
+315
View File
@@ -0,0 +1,315 @@
import Quickshell
import QtQuick
import ZShell.Config
import ZShell.Internal
import qs.Components
import qs.Helpers
import qs.Modules.Bar.Popouts
CustomMouseArea {
id: root
required property Item bar
required property real borderThickness
property bool dashboardShortcutActive
property point dragStart
required property Drawing drawing
required property EdgeGeometry geometry
property bool osdShortcutActive
required property Panels panels
required property Wrapper popouts
required property ShellScreen screen
required property ScreenState screenState
property bool setInitialPoint: false
property bool singleGestureTriggered: false
property bool utilitiesShortcutActive
function inBottomPanel(panel: Item, x: real, y: real): bool {
const panelHeight = panel.height * (1 - (panel.offsetScale ?? 0));
return y > root.height - (panelHeight + geometry.insetBottom(borderThickness)) && withinPanelWidth(panel, x, y);
}
function inLeftPanel(panel: Item, x: real, y: real): bool {
return x < panel.x + panel.width + geometry.insetLeft(borderThickness) && withinPanelHeight(panel, x, y);
}
function inPopoutArea(x: real, y: real): bool {
const panel = panels.popoutsWrapper;
if (!geometry.horizontal)
return inLeftPanel(panel, x, y);
return withinPanelWidth(panel, x, y) && withinPanelHeight(panel, x, y);
}
function inRightPanel(panel: Item, x: real, y: real): bool {
return x > panel.x + geometry.insetLeft(borderThickness) && withinPanelHeight(panel, x, y);
}
function inTopPanel(panel: Item, x: real, y: real): bool {
const panelHeight = panel.height * (1 - (panel.offsetScale ?? 0));
return y < geometry.insetTop(borderThickness) + panelHeight && withinPanelWidth(panel, x, y);
}
function withinPanelHeight(panel: Item, x: real, y: real): bool {
const panelY = panel.y + geometry.insetTop(borderThickness);
return y >= panelY && y <= panelY + panel.height;
}
function withinPanelWidth(panel: Item, x: real, y: real): bool {
const panelX = panel.x + geometry.insetLeft(borderThickness);
return x >= panelX && x <= panelX + panel.width;
}
acceptedButtons: bar.fullscreen ? Qt.NoButton : Qt.AllButtons
anchors.fill: parent
hoverEnabled: true
onContainsMouseChanged: {
if (!containsMouse) {
if (!osdShortcutActive) {
screenState.osd = false;
panels.osd.hovered = false;
}
if (!popouts.currentName.startsWith("traymenu") || Config.bar.tray.showOnHover) {
popouts.hasCurrent = false;
}
if (Config.bar.autoHide || bar.fullscreen)
bar.isHovered = false;
if (screenState.dashboard && Config.dashboard.showOnHover)
screenState.dashboard = false;
}
}
onPositionChanged: event => {
const x = event.x;
const y = event.y;
const dragX = x - dragStart.x;
const dragY = y - dragStart.y;
if (screenState.isDrawing) {
if (pressed) {
drawing.content.appendPoint(x, y);
} else {
if (inLeftPanel(panels.drawing, x, y)) {
if (!(pressedButtons & Qt.LeftButton)) {
panels.drawing.expand();
drawing.content.hideHover();
Cursor.clearOverride();
}
} else {
Cursor.setOverride(Qt.BlankCursor);
drawing.content.showHover(x, y);
}
}
if (drawing.content.hoverVisible)
drawing.content.showHover(x, y);
return;
}
if (root.bar.fullscreen && !root.bar.isHovered)
unhideTimer.start();
if (pressed && dragStart.y >= screen.height - Config.bar.border && dragStart.x > screen.width / 5 && dragY < -Config.launcher.dragThreshold)
screenState.launcher = true;
const entryAt = bar.entryAt(geometry.axisPos(x, y));
const showDashboard = Config.dashboard.showOnHover && inTopPanel(panels.dashboardWrapper, x, y) && (!entryAt || entryAt === "activeWindow");
if (!dashboardShortcutActive) {
screenState.dashboard = showDashboard;
} else if (showDashboard) {
dashboardShortcutActive = false;
}
if (pressed && geometry.insetTop(borderThickness) > dragStart.y && withinPanelWidth(panels.dashboardWrapper, x, y)) {
if (dragY > Config.dashboard.dragThreshold)
screenState.dashboard = true;
else if (dragY < -Config.dashboard.dragThreshold)
screenState.dashboard = false;
}
if (pressed && geometry.insetTop(borderThickness) > dragStart.y && !inTopPanel(panels.dashboardWrapper, dragStart.x, dragStart.y)) {
if (dragY > 20) {
screenState.settings = true;
} else if (dragY < -20) {
screenState.settings = false;
}
}
if (pressed && dragStart.y > screen.height - geometry.insetBottom(borderThickness) && dragStart.x < screen.width / 5 && dragY < -50)
screenState.clipboard = true;
if (pressed && !Config.dock.hoverToReveal && dragStart.y > screen.height - geometry.insetTop(borderThickness) && dragStart.x > screen.width / 5 && !screenState.launcher)
if (dragY < -10)
screenState.dock = true;
if (pressed && dragStart.x > screen.width - Config.bar.border && dragStart.y < (screen.height / 2) && dragX < -Config.sidebar.dragThreshold)
screenState.sidebar = true;
if (pressed && dragStart.x >= screen.width - Config.bar.border && dragStart.y > (screen.height / 2) && dragX < -20)
Hypr.dispatch(`hl.dsp.focus({ workspace = 'r+1', on_current_monitor = true })`);
if (pressed && dragStart.x <= Config.bar.border && dragX > 20)
Hypr.dispatch(`hl.dsp.focus({ workspace = 'r-1', on_current_monitor = true })`);
if (!screenState.bar && Config.bar.autoHide && geometry.barContains(x, y, true))
bar.isHovered = true;
if (panels.sidebar.offsetScale === 1) {
const showOsd = inRightPanel(panels.osdWrapper, x, y);
if (showOsd) {
osdShortcutActive = false;
panels.osd.hovered = true;
}
} else {
const outOfSidebar = x < width - panels.sidebar.width * (1 - panels.sidebar.offsetScale);
const showOsd = outOfSidebar && inRightPanel(panels.osdWrapper, x, y);
if (!osdShortcutActive) {
screenState.osd = showOsd;
panels.osd.hovered = showOsd;
} else if (showOsd) {
osdShortcutActive = false;
panels.osd.hovered = true;
}
}
if (Config.dock.enable && Config.dock.hoverToReveal && !screenState.dock && !screenState.launcher && inBottomPanel(panels.dock, x, y))
screenState.dock = true;
if (geometry.barContains(x, y))
bar.checkPopout(geometry.axisPos(x, y));
}
onPressed: event => {
dragStart = Qt.point(event.x, event.y);
if (screenState.isDrawing) {
if (event.button & Qt.RightButton) {
drawing.content.clear();
return;
}
if (!inLeftPanel(panels.drawing, mouseX, mouseY))
drawing.content.beginStroke(event.x, event.y);
}
}
onPressedChanged: {
if (!pressed) {
if (screenState.isDrawing) {
setInitialPoint = false;
drawing.content.endStroke();
if (!inLeftPanel(panels.drawing, mouseX, mouseY))
drawing.content.showHover(mouseX, mouseY);
}
} else {
if (screenState.isDrawing) {
panels.drawing.collapse();
}
}
}
Timer {
id: unhideTimer
interval: Config.bar.revealDelay
repeat: false
running: false
onTriggered: {
if (root.containsMouse)
root.bar.isHovered = true;
}
}
Connections {
function onDashboardChanged(): void {
if (root.screenState.dashboard) {
const inDashboardArea = root.inTopPanel(root.panels.dashboardWrapper, root.mouseX, root.mouseY);
if (!inDashboardArea) {
root.dashboardShortcutActive = true;
}
root.screenState.launcher = false;
root.screenState.settings = false;
root.screenState.sidebar = false;
root.screenState.resources = false;
root.popouts.hasCurrent = false;
} else {
root.dashboardShortcutActive = false;
}
}
function onIsDrawingChanged(): void {
if (root.screenState.isDrawing) {
Cursor.setOverride(Qt.BlankCursor);
} else {
Cursor.clearOverride();
}
}
function onLauncherChanged(): void {
if (!root.screenState.launcher) {
root.dashboardShortcutActive = false;
root.osdShortcutActive = false;
root.utilitiesShortcutActive = false;
const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY);
if (!inOsdArea) {
root.screenState.osd = false;
root.panels.osd.hovered = false;
}
}
if (root.screenState.launcher) {
root.screenState.dashboard = false;
root.screenState.dock = false;
root.screenState.settings = false;
}
}
function onOsdChanged(): void {
if (root.screenState.osd) {
const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY);
if (!inOsdArea) {
root.osdShortcutActive = true;
}
} else {
root.osdShortcutActive = false;
}
}
function onResourcesChanged(): void {
if (root.screenState.resources) {
if (root.popouts.currentName.startsWith("audio") || root.popouts.currentName.startsWith("updates"))
root.popouts.hasCurrent = false;
root.screenState.settings = false;
root.screenState.dashboard = false;
}
}
function onSettingsChanged(): void {
if (root.screenState.settings) {
root.screenState.resources = false;
root.screenState.dashboard = false;
root.screenState.sidebar = false;
root.panels.popouts.hasCurrent = false;
root.screenState.launcher = false;
}
}
function onSidebarChanged(): void {
if (root.screenState.sidebar) {
if (root.panels.dashboardWrapper.x + root.panels.dashboardWrapper.width > root.panels.sidebar.x)
root.screenState.dashboard = false;
root.screenState.settings = false;
root.popouts.hasCurrent = false;
}
}
target: root.screenState
}
}
+17 -21
View File
@@ -37,12 +37,12 @@ Item {
readonly property alias resources: resources readonly property alias resources: resources
readonly property alias resourcesWrapper: resourcesWrapper readonly property alias resourcesWrapper: resourcesWrapper
required property ShellScreen screen required property ShellScreen screen
required property ScreenState screenState
readonly property alias settings: settings readonly property alias settings: settings
readonly property alias settingsWrapper: settingsWrapper readonly property alias settingsWrapper: settingsWrapper
readonly property alias sidebar: sidebar readonly property alias sidebar: sidebar
readonly property alias toasts: toasts readonly property alias toasts: toasts
readonly property alias utilities: utilities readonly property alias utilities: utilities
required property PersistentProperties visibilities
anchors.bottomMargin: geometry.insetBottom(borderThickness) anchors.bottomMargin: geometry.insetBottom(borderThickness)
anchors.fill: parent anchors.fill: parent
@@ -64,7 +64,7 @@ Item {
id: resources id: resources
position: root.geometry.position position: root.geometry.position
visibilities: root.visibilities screenState: root.screenState
} }
} }
@@ -84,8 +84,8 @@ Item {
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
screen: root.screen screen: root.screen
screenState: root.screenState
sidebarOrSessionVisible: sidebar.visible sidebarOrSessionVisible: sidebar.visible
visibilities: root.visibilities
} }
} }
@@ -96,7 +96,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
drawing: root.drawingItem drawing: root.drawingItem
screen: root.screen screen: root.screen
visibilities: root.visibilities screenState: root.screenState
} }
Popouts.ClipWrapper { Popouts.ClipWrapper {
@@ -121,8 +121,8 @@ Item {
anchors.right: parent.right anchors.right: parent.right
anchors.top: parent.top anchors.top: parent.top
panels: root panels: root
screenState: root.screenState
sidebarPanel: sidebar sidebarPanel: sidebar
visibilities: root.visibilities
} }
Launcher.Wrapper { Launcher.Wrapper {
@@ -132,7 +132,7 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
panels: root panels: root
screen: root.screen screen: root.screen
visibilities: root.visibilities screenState: root.screenState
} }
Utils.Wrapper { Utils.Wrapper {
@@ -140,9 +140,8 @@ Item {
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.right: parent.right anchors.right: parent.right
popouts: popouts screenState: root.screenState
sidebar: sidebar sidebar: sidebar
visibilities: root.visibilities
} }
Item { Item {
@@ -150,13 +149,11 @@ Item {
property real offsetScale: dashboard.shouldBeActive ? 0 : 1 property real offsetScale: dashboard.shouldBeActive ? 0 : 1
anchors.bottom: root.geometry.barOnLeft || root.geometry.barOnBottom ? parent.bottom : undefined anchors.horizontalCenter: parent.horizontalCenter
anchors.left: root.geometry.barOnLeft ? parent.left : undefined anchors.top: parent.top
anchors.right: root.geometry.barOnLeft ? undefined : parent.right clip: offsetScale > 0
anchors.top: root.geometry.barOnBottom || root.geometry.barOnLeft ? undefined : parent.top implicitHeight: dashboard.implicitHeight * (1 - offsetScale)
clip: true implicitWidth: dashboard.implicitWidth
implicitHeight: root.geometry.horizontal ? dashboard.implicitHeight * (1 - offsetScale) : dashboard.implicitHeight
implicitWidth: root.geometry.horizontal ? dashboard.implicitWidth : dashboard.implicitWidth * (1 - offsetScale)
Behavior on offsetScale { Behavior on offsetScale {
Anim { Anim {
@@ -169,8 +166,7 @@ Item {
id: dashboard id: dashboard
offsetScale: dashboardWrapper.offsetScale offsetScale: dashboardWrapper.offsetScale
position: root.geometry.position screenState: root.screenState
visibilities: root.visibilities
} }
} }
@@ -181,7 +177,7 @@ Item {
anchors.right: parent.right anchors.right: parent.right
anchors.top: notifications.bottom anchors.top: notifications.bottom
panels: root panels: root
visibilities: root.visibilities screenState: root.screenState
} }
Item { Item {
@@ -197,7 +193,7 @@ Item {
anchors.verticalCenterOffset: (-implicitHeight - 5 - ((root.height - implicitHeight) / 2)) * offsetScale anchors.verticalCenterOffset: (-implicitHeight - 5 - ((root.height - implicitHeight) / 2)) * offsetScale
panels: root panels: root
screen: root.screen screen: root.screen
visibilities: root.visibilities screenState: root.screenState
} }
} }
@@ -208,7 +204,7 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
panels: root panels: root
screen: root.screen screen: root.screen
visibilities: root.visibilities screenState: root.screenState
} }
Clipboard.Wrapper { Clipboard.Wrapper {
@@ -217,6 +213,6 @@ Item {
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.left: parent.left anchors.left: parent.left
screen: root.screen screen: root.screen
visibilities: root.visibilities screenState: root.screenState
} }
} }
+89 -106
View File
@@ -51,49 +51,47 @@ CustomWindow {
readonly property alias menuRegion: regions.menuPopoutRegion readonly property alias menuRegion: regions.menuPopoutRegion
readonly property HyprlandMonitor monitor: Hypr.monitorFor(screen) readonly property HyprlandMonitor monitor: Hypr.monitorFor(screen)
property var root: Quickshell.shellDir property var root: Quickshell.shellDir
readonly property ScreenState screenState: ShellState.forScreen(screen)
readonly property real sdfBorderOffset: 2 * fsTransitionProg readonly property real sdfBorderOffset: 2 * fsTransitionProg
readonly property real shadowOpacity: 0.7 * (1 - (bar.isHovered ? 0 : fsTransitionProg)) readonly property real shadowOpacity: 0.5 * (1 - (bar.isHovered ? 0 : fsTransitionProg))
property color surfaceColor: Colors.tPalette.m3surface property color surfaceColor: Colors.tPalette.m3surface
WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.settings ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None WlrLayershell.keyboardFocus: screenState.screenshot ? WlrKeyboardFocus.Exclusive : screenState.launcher || screenState.settings || screenState.sidebar ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
WlrLayershell.layer: (fsTransitionProg > 0 && Config.general.showOverFullscreen) || (hasSpecialWorkspace && hasFullscreenOnNormalWs) ? WlrLayer.Overlay : WlrLayer.Top WlrLayershell.layer: (fsTransitionProg > 0 && Config.general.showOverFullscreen) || (hasSpecialWorkspace && hasFullscreenOnNormalWs) ? WlrLayer.Overlay : WlrLayer.Top
color: "transparent" color: "transparent"
contentItem.focus: true mask: screenState.isDrawing || screenState.screenshot ? null : (hasFullscreen && !bar.isHovered ? emptyRegion : regions)
mask: visibilities.isDrawing ? null : (hasFullscreen && !bar.isHovered ? emptyRegion : regions)
name: "Bar" name: "Bar"
Behavior on fsTransitionProg { Behavior on fsTransitionProg {
Anim { Anim {}
}
} }
Behavior on surfaceColor { Behavior on surfaceColor {
CAnim { CAnim {}
}
} }
contentItem.Keys.onEscapePressed: { contentItem.Keys.onEscapePressed: {
if (Config.bar.autoHide) if (Config.bar.autoHide)
visibilities.bar = false; screenState.bar = false;
visibilities.launcher = false; screenState.launcher = false;
visibilities.sidebar = false; screenState.sidebar = false;
visibilities.dashboard = false; screenState.dashboard = false;
visibilities.osd = false; screenState.osd = false;
visibilities.settings = false; screenState.settings = false;
visibilities.resources = false; screenState.resources = false;
visibilities.dock = false; screenState.dock = false;
visibilities.clipboard = false; screenState.clipboard = false;
panels.popouts.hasCurrent = false; panels.popouts.hasCurrent = false;
} }
onHasFullscreenChanged: { onHasFullscreenChanged: {
visibilities.launcher = false; screenState.launcher = false;
visibilities.sidebar = false; screenState.sidebar = false;
visibilities.dashboard = false; screenState.dashboard = false;
visibilities.osd = false; screenState.osd = false;
visibilities.settings = false; screenState.settings = false;
visibilities.resources = false; screenState.resources = false;
visibilities.clipboard = false; screenState.clipboard = false;
visibilities.dock = false; screenState.dock = false;
panels.popouts.hasCurrent = false; panels.popouts.hasCurrent = false;
} }
@@ -146,53 +144,40 @@ CustomWindow {
id: focusGrab id: focusGrab
active: { active: {
const v = visibilities; const s = root.screenState;
const conf = Config; const c = Config;
if (!N.PopupManager.closed && panels.popouts.hasCurrent)
if ((s.launcher && c.launcher.enabled) || (s.sidebar && c.sidebar.enabled) || (s.resources && c.dashboard.performance.enabled) || (s.clipboard && c.clipboard.enabled))
return true; return true;
if ((v.launcher && conf.launcher.enabled) || (v.sidebar && conf.sidebar.enabled) || (v.resources && conf.dashboard.performance.enabled) || (v.dashboard && conf.dashboard.enabled) || v.settings || (v.clipboard && conf.clipboard.enabled) || (v.dock && conf.dock.enabled)) if (s.dashboard && !c.dashboard.showOnHover && c.dashboard.enabled)
return true; return true;
if (panels.popouts.hasCurrent && panels.popouts.currentName.startsWith("traymenu") && (!conf.bar.tray.showOnHover || (panels.popouts.current as StackView)?.depth > 1)) if (!c.dock.hoverToReveal && s.dock && c.dock.enable)
return true;
if (panels.popouts.currentName.startsWith("traymenu") && (panels.popouts.current as StackView)?.depth > 1)
return true;
if (s.settings)
return true; return true;
return false; return false;
} }
windows: [root] windows: [root]
onCleared: { onCleared: {
visibilities.launcher = false; root.screenState.launcher = false;
visibilities.sidebar = false; root.screenState.sidebar = false;
visibilities.dashboard = false; root.screenState.dashboard = false;
visibilities.osd = false; root.screenState.osd = false;
visibilities.settings = false; root.screenState.settings = false;
visibilities.resources = false; root.screenState.resources = false;
visibilities.clipboard = false; root.screenState.clipboard = false;
visibilities.dock = false; root.screenState.dock = false;
panels.popouts.hasCurrent = 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)
}
Binding { Binding {
property: "bar" property: "bar"
target: visibilities target: root.screenState
value: visibilities.sidebar || visibilities.dashboard || visibilities.osd || (!Config.bar.hideWhenNotif && visibilities.notif) || visibilities.resources || visibilities.settings || bar.isHovered value: root.screenState.sidebar || root.screenState.dashboard || root.screenState.osd || (!Config.bar.hideWhenNotif && NotifServer.popups.length > 0) || root.screenState.resources || root.screenState.settings || bar.isHovered
when: Config.bar.autoHide when: Config.bar.autoHide
} }
@@ -200,23 +185,19 @@ CustomWindow {
id: surface id: surface
anchors.fill: parent anchors.fill: parent
layer.enabled: true
opacity: root.surfaceColor.a opacity: root.surfaceColor.a
layer.effect: MultiEffect { BlobGroup2 {
blurMax: 32
shadowColor: Qt.alpha(Colors.palette.m3shadow, Math.max(0, root.shadowOpacity))
shadowEnabled: true
}
BlobGroup {
id: blobGroup id: blobGroup
anchors.fill: parent
color: root.surfaceColor color: root.surfaceColor
shadowColor: Qt.alpha(Colors.palette.m3shadow, Math.max(0, root.shadowOpacity))
shadowRadius: 18
smoothing: Config.bar.smoothing smoothing: Config.bar.smoothing
} }
BlobInvertedRect { BlobInvertedRect2 {
anchors.fill: parent anchors.fill: parent
anchors.margins: -50 anchors.margins: -50
borderBottom: geometry.insetBottom(root.borderThickness) - anchors.margins - root.sdfBorderOffset borderBottom: geometry.insetBottom(root.borderThickness) - anchors.margins - root.sdfBorderOffset
@@ -266,6 +247,7 @@ CustomWindow {
PanelBg { PanelBg {
id: osdBg id: osdBg
atRestOverride: panels.osd.offsetScale < 0.99
deformAmount: 0.1 deformAmount: 0.1
implicitHeight: panels.osd.height implicitHeight: panels.osd.height
implicitWidth: panels.osd.width implicitWidth: panels.osd.width
@@ -277,6 +259,7 @@ CustomWindow {
PanelBg { PanelBg {
id: notifsBg id: notifsBg
atRestOverride: panel.implicitHeight > 0
panel: panels.notifications panel: panels.notifications
radius: Tokens.rounding.large radius: Tokens.rounding.large
} }
@@ -304,21 +287,23 @@ CustomWindow {
y: panels.popoutsWrapper.y + panels.popouts.y + geometry.insetTop(root.borderThickness) - (geometry.barOnTop ? panels.popouts.height * extraExtent : 0) y: panels.popoutsWrapper.y + panels.popouts.y + geometry.insetTop(root.borderThickness) - (geometry.barOnTop ? panels.popouts.height * extraExtent : 0)
Behavior on extraExtent { Behavior on extraExtent {
Anim { Anim {}
}
} }
} }
PanelBg { PanelBg {
id: resourcesBg id: resourcesBg
property real extraExtent: 0.2
atRestOverride: panels.resources.offsetScale < 0.99
deformAmount: 0.05 deformAmount: 0.05
implicitHeight: panels.resources.height implicitHeight: panels.resources.height * (1 + extraExtent)
implicitWidth: panels.resources.width implicitWidth: panels.resources.width * (1 + extraExtent)
panel: panels.resourcesWrapper panel: panels.resourcesWrapper
radius: Tokens.rounding.largeIncreased + Tokens.padding.small radius: Tokens.rounding.largeIncreased + Tokens.padding.small
x: panels.resourcesWrapper.x + panels.resources.x + geometry.insetLeft(root.borderThickness) x: panels.resourcesWrapper.x + panels.resources.x + geometry.insetLeft(root.borderThickness) - panels.resources.width * extraExtent
y: panels.resourcesWrapper.y + panels.resources.y + geometry.insetTop(root.borderThickness) y: panels.resourcesWrapper.y + panels.resources.y + geometry.insetTop(root.borderThickness) - panels.resources.height * extraExtent
} }
PanelBg { PanelBg {
@@ -348,9 +333,15 @@ CustomWindow {
PanelBg { PanelBg {
id: clipboardBg id: clipboardBg
property real extraExtent: 0.2
deformAmount: 0.03 deformAmount: 0.03
implicitHeight: panels.clipboard.height * (1 + extraExtent)
implicitWidth: panels.clipboard.width * (1 + extraExtent)
panel: panels.clipboard panel: panels.clipboard
radius: 29 radius: 29
x: panels.clipboard.x + geometry.insetLeft(root.borderThickness) - panels.clipboard.width * extraExtent
y: panels.clipboard.y + geometry.insetTop(root.borderThickness)
} }
} }
@@ -358,37 +349,11 @@ CustomWindow {
id: drawing id: drawing
anchors.fill: parent anchors.fill: parent
layer.enabled: true screenState: root.screenState
visibilities: visibilities
z: 2 z: 2
layer.effect: MultiEffect {
maskEnabled: true
maskInverted: true
maskSource: maskSource
}
} }
Item { Interactions2 {
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 id: interactions
anchors.fill: parent anchors.fill: parent
@@ -400,7 +365,7 @@ CustomWindow {
panels: panels panels: panels
popouts: panels.popouts popouts: panels.popouts
screen: root.screen screen: root.screen
visibilities: visibilities screenState: root.screenState
Panels { Panels {
id: panels id: panels
@@ -410,7 +375,7 @@ CustomWindow {
drawingItem: drawing drawingItem: drawing
geometry: geometry geometry: geometry
screen: root.screen screen: root.screen
visibilities: visibilities screenState: root.screenState
clipboard.transform: Matrix4x4 { clipboard.transform: Matrix4x4 {
matrix: clipboardBg.deformMatrix matrix: clipboardBg.deformMatrix
@@ -453,22 +418,40 @@ CustomWindow {
anchors.bottom: geometry.barOnBottom ? parent.bottom : undefined anchors.bottom: geometry.barOnBottom ? parent.bottom : undefined
anchors.left: parent.left anchors.left: parent.left
anchors.top: geometry.barOnBottom ? undefined : parent.top anchors.top: geometry.barOnBottom ? undefined : parent.top
enabled: !visibilities.isDrawing enabled: !screenState.isDrawing
fullscreen: root.hasFullscreen fullscreen: root.hasFullscreen
height: geometry.horizontal ? implicitHeight : parent.height height: geometry.horizontal ? implicitHeight : parent.height
popouts: panels.popouts popouts: panels.popouts
popoutsWrapper: panels.popoutsWrapper popoutsWrapper: panels.popoutsWrapper
position: geometry.position position: geometry.position
screen: root.screen screen: root.screen
visibilities: visibilities screenState: root.screenState
width: geometry.horizontal ? parent.width : implicitWidth width: geometry.horizontal ? parent.width : implicitWidth
} }
} }
component PanelBg: BlobRect { LazyLoader {
id: loader
property bool closing: !root.screenState.screenshot
onClosingChanged: if (!closing)
activeAsync = true
Picker {
loader: loader
parent: root.contentItem
screen: root.screen
screenState: root.screenState
}
}
component PanelBg: BlobRect2 {
property real deformAmount: 0.15 property real deformAmount: 0.15
required property Item panel required property Item panel
atRestOverride: panel?.offsetScale < 0.99 ?? false
damping: 28.3
deformScale: (deformAmount * Config.appearance.deform.scale) / 10000 deformScale: (deformAmount * Config.appearance.deform.scale) / 10000
group: blobGroup group: blobGroup
implicitHeight: panel.height implicitHeight: panel.height
+5 -10
View File
@@ -13,21 +13,17 @@ import qs.Paths
Singleton { Singleton {
id: root id: root
readonly property M3Palette current: M3Palette { readonly property M3Palette current: M3Palette {}
}
property bool currentLight property bool currentLight
property string flavour property string flavour
readonly property bool light: showPreview ? previewLight : currentLight readonly property bool light: showPreview ? previewLight : currentLight
readonly property M3Palette palette: showPreview ? preview : current readonly property M3Palette palette: showPreview ? preview : current
readonly property M3Palette preview: M3Palette { readonly property M3Palette preview: M3Palette {}
}
property bool previewLight property bool previewLight
property string scheme property string scheme
property bool showPreview property bool showPreview
readonly property M3TPalette tPalette: M3TPalette { readonly property M3TPalette tPalette: M3TPalette {}
} readonly property Transparency transparency: Transparency {}
readonly property Transparency transparency: Transparency {
}
readonly property alias wallLuminance: analyser.luminance readonly property alias wallLuminance: analyser.luminance
function alterColor(c: color, a: real, layer: int): color { function alterColor(c: color, a: real, layer: int): color {
@@ -112,8 +108,7 @@ Singleton {
function setMode(mode: string): void { function setMode(mode: string): void {
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--mode", mode]); Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--mode", mode]);
Config.general.color.mode = mode; Config.colors.mode = mode;
Config.save();
} }
function swapRG(c: color): color { function swapRG(c: color): color {
+2 -1
View File
@@ -119,7 +119,8 @@ ColumnLayout {
length: count length: count
}, () => null); }, () => null);
return forecast.slice(0, count); const start = Math.max(0, Weather.currentHourIndex);
return forecast.slice(start, start + count);
} }
ColumnLayout { ColumnLayout {
-88
View File
@@ -1,88 +0,0 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Wayland
import Quickshell.Io
import Quickshell.Hyprland
import ZShell
import qs.Components
Scope {
LazyLoader {
id: root
property bool closing
property bool freeze
Variants {
model: Quickshell.screens
PanelWindow {
id: win
required property ShellScreen modelData
WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.keyboardFocus: root.closing ? WlrKeyboardFocus.None : WlrKeyboardFocus.Exclusive
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.namespace: "areapicker"
color: "transparent"
mask: root.closing ? empty : null
screen: modelData
anchors {
bottom: true
left: true
right: true
top: true
}
Region {
id: empty
}
Picker {
loader: root
screen: win.modelData
}
}
}
}
IpcHandler {
function open(): void {
root.freeze = false;
root.closing = false;
root.activeAsync = true;
}
function openFreeze(): void {
root.freeze = true;
root.closing = false;
root.activeAsync = true;
}
target: "picker"
}
CustomShortcut {
name: "screenshot"
onPressed: {
root.freeze = false;
root.closing = false;
root.activeAsync = true;
}
}
CustomShortcut {
name: "screenshotFreeze"
onPressed: {
root.freeze = true;
root.closing = false;
root.activeAsync = true;
}
}
}
-37
View File
@@ -1,37 +0,0 @@
pragma Singleton
import Quickshell
import QtQuick
import ZShell.Services
import ZShell.Config
Singleton {
id: root
readonly property bool enabled: service.enabled
readonly property int end: Config.general.color.scheduleHyprsunsetEnd
property bool manualToggle: false
readonly property int start: Config.general.color.scheduleHyprsunsetStart
readonly property int temp: Config.general.color.hyprsunsetTemp
function checkStartup(): void {
if (!Config.general.color.scheduleHyprsunset)
return;
service.apply();
}
function toggleNightLight(): void {
service.manualToggle = true;
service.toggle();
}
HyprsunsetManager {
id: service
activeAuto: Config.general.color.scheduleHyprsunset
endTime: root.end
startTime: root.start
temp: root.temp
}
}
+6 -6
View File
@@ -11,14 +11,14 @@ import qs.Services
Singleton { Singleton {
id: root id: root
readonly property int darkEnd: Config.general.color.scheduleDarkEnd readonly property int darkEnd: Config.colors.scheduleDarkEnd
readonly property int darkStart: Config.general.color.scheduleDarkStart readonly property int darkStart: Config.colors.scheduleDarkStart
readonly property bool enabled: Config.general.color.scheduleDark && Config.general.color.schemeGeneration readonly property bool enabled: Config.colors.scheduleDark && Config.colors.schemeGen
function applyDarkMode() { function applyDarkMode() {
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--mode", "dark"]); Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--mode", "dark"]);
Config.general.color.mode = "dark"; Config.colors.mode = "dark";
Quickshell.execDetached(["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", "'prefer-dark'"]); Quickshell.execDetached(["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", "'prefer-dark'"]);
@@ -30,13 +30,13 @@ Singleton {
function applyLightMode() { function applyLightMode() {
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--mode", "light"]); Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--mode", "light"]);
Config.general.color.mode = "light"; Config.colors.mode = "light";
Quickshell.execDetached(["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", "'prefer-light'"]); Quickshell.execDetached(["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", "'prefer-light'"]);
Quickshell.execDetached(["sh", "-c", `sed -i 's/color_scheme_path=\\(.*\\)Dark.colors/color_scheme_path=\\1Light.colors/' ${Paths.home}/.config/qt6ct/qt6ct.conf`]); Quickshell.execDetached(["sh", "-c", `sed -i 's/color_scheme_path=\\(.*\\)Dark.colors/color_scheme_path=\\1Light.colors/' ${Paths.home}/.config/qt6ct/qt6ct.conf`]);
if (Config.general.color.neovimColors) if (Config.colors.neovimColors)
Quickshell.execDetached(["sed", "-i", "'s/\\(vim.cmd.colorscheme \\).*/\\1\"onelight\"/'", "~/.config/nvim/lua/config/load-colorscheme.lua"]); Quickshell.execDetached(["sed", "-i", "'s/\\(vim.cmd.colorscheme \\).*/\\1\"onelight\"/'", "~/.config/nvim/lua/config/load-colorscheme.lua"]);
} }
-242
View File
@@ -1,242 +0,0 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import ZShell.Internal
import ZShell.Config
Singleton {
id: root
property real _downloadSpeed: 0
property real _downloadTotal: 0
property real _initialRxBytes: 0
property real _initialTxBytes: 0
property bool _initialized: false
property real _prevRxBytes: 0
property real _prevTimestamp: 0
property real _prevTxBytes: 0
property real _uploadSpeed: 0
property real _uploadTotal: 0
readonly property CircularBuffer downloadBuffer: _downloadBuffer
readonly property real downloadSpeed: _downloadSpeed
readonly property real downloadTotal: _downloadTotal
readonly property int historyLength: Config.dashboard.performance.showVram ? 70 : 30
property int refCount: 0
readonly property CircularBuffer uploadBuffer: _uploadBuffer
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)) {
return {
value: 0,
unit: "B/s"
};
}
if (bytes < 1024) {
return {
value: bytes,
unit: "B/s"
};
} else if (bytes < 1024 * 1024) {
return {
value: bytes / 1024,
unit: "KB/s"
};
} else if (bytes < 1024 * 1024 * 1024) {
return {
value: bytes / (1024 * 1024),
unit: "MB/s"
};
} else {
return {
value: bytes / (1024 * 1024 * 1024),
unit: "GB/s"
};
}
}
function formatBytesTotal(bytes: real): var {
// Handle negative or invalid values
if (bytes < 0 || isNaN(bytes) || !isFinite(bytes)) {
return {
value: 0,
unit: "B"
};
}
if (bytes < 1024) {
return {
value: bytes,
unit: "B"
};
} else if (bytes < 1024 * 1024) {
return {
value: bytes / 1024,
unit: "KB"
};
} else if (bytes < 1024 * 1024 * 1024) {
return {
value: bytes / (1024 * 1024),
unit: "MB"
};
} else {
return {
value: bytes / (1024 * 1024 * 1024),
unit: "GB"
};
}
}
function parseNetDev(content: string): var {
const lines = content.split("\n");
let totalRx = 0;
let totalTx = 0;
for (let i = 2; i < lines.length; i++) {
const line = lines[i].trim();
if (!line)
continue;
const parts = line.split(/\s+/);
if (parts.length < 10)
continue;
const iface = parts[0].replace(":", "");
// Skip loopback interface
if (iface === "lo")
continue;
const rxBytes = parseFloat(parts[1]) || 0;
const txBytes = parseFloat(parts[9]) || 0;
totalRx += rxBytes;
totalTx += txBytes;
}
return {
rx: totalRx,
tx: totalTx
};
}
CircularBuffer {
id: _downloadBuffer
capacity: root.historyLength + 1
}
CircularBuffer {
id: _uploadBuffer
capacity: root.historyLength + 1
}
FileView {
id: netDevFile
path: "/proc/net/dev"
}
Timer {
interval: Config.dashboard.resourceUpdateInterval
repeat: true
running: true
triggeredOnStart: true
onTriggered: {
if (root.refCount <= 0) {
root.resetSampling();
return;
}
netDevFile.reload();
const content = netDevFile.text();
if (!content)
return;
const data = root.parseNetDev(content);
const now = Date.now();
if (!root._initialized) {
root._initialRxBytes = data.rx;
root._initialTxBytes = data.tx;
root._prevRxBytes = data.rx;
root._prevTxBytes = data.tx;
root._prevTimestamp = now;
root._initialized = true;
return;
}
const timeDelta = (now - root._prevTimestamp) / 1000; // seconds
if (timeDelta > 0) {
// Calculate byte deltas
let rxDelta = data.rx - root._prevRxBytes;
let txDelta = data.tx - root._prevTxBytes;
// Handle counter overflow (when counters wrap around from max to 0)
// This happens when counters exceed 32-bit or 64-bit limits
if (rxDelta < 0) {
// Counter wrapped around - assume 64-bit counter
rxDelta += Math.pow(2, 64);
}
if (txDelta < 0) {
txDelta += Math.pow(2, 64);
}
// Calculate speeds
root._downloadSpeed = rxDelta / timeDelta;
root._uploadSpeed = txDelta / timeDelta;
if (root._downloadSpeed >= 0 && isFinite(root._downloadSpeed))
_downloadBuffer.push(root._downloadSpeed);
if (root._uploadSpeed >= 0 && isFinite(root._uploadSpeed))
_uploadBuffer.push(root._uploadSpeed);
}
// Calculate totals with overflow handling
let downTotal = data.rx - root._initialRxBytes;
let upTotal = data.tx - root._initialTxBytes;
// Handle counter overflow for totals
if (downTotal < 0) {
downTotal += Math.pow(2, 64);
}
if (upTotal < 0) {
upTotal += Math.pow(2, 64);
}
root._downloadTotal = downTotal;
root._uploadTotal = upTotal;
root._prevRxBytes = data.rx;
root._prevTxBytes = data.tx;
root._prevTimestamp = now;
}
}
}
+39
View File
@@ -0,0 +1,39 @@
pragma Singleton
import Quickshell
import QtQuick
import ZShell.Services
import ZShell.Config
Singleton {
id: root
readonly property bool enabled: service.enabled
readonly property int end: Config.display.nightlight.scheduleEnd
property bool manualToggle: false
readonly property int start: Config.display.nightlight.scheduleStart
readonly property int temp: Config.display.nightlight.temp
function checkStartup(): void {
if (!Config.display.nightlight.schedule)
return;
service.apply();
}
function toggleNightLight(): void {
service.manualToggle = true;
service.toggle(true);
}
NightlightManager {
id: service
activeAuto: Config.display.nightlight.schedule
endTime: root.end
startTime: root.start
temp: root.temp
fadeDuration: Config.display.nightlight.fadeDuration / 1000
useNativeNightlight: Config.display.nightlight.useNative
}
}
+100 -60
View File
@@ -41,8 +41,9 @@ Item {
property real rsy: Math.min(sy, ey) property real rsy: Math.min(sy, ey)
readonly property real scaleRatio: Hypr.monitorFor(screen).scale readonly property real scaleRatio: Hypr.monitorFor(screen).scale
required property ShellScreen screen required property ShellScreen screen
required property ScreenState screenState
property real sh: Math.abs(sy - ey) property real sh: Math.abs(sy - ey)
readonly property color shadowColor: Hypr.options.decoration.shadow.color readonly property color shadowColor: Hypr.options.decoration.shadow?.color ?? "black"
readonly property bool shadowEnabled: Hypr.options.decoration.shadow.enabled readonly property bool shadowEnabled: Hypr.options.decoration.shadow.enabled
readonly property var shadowOffset: Hypr.options.decoration.shadow.offset readonly property var shadowOffset: Hypr.options.decoration.shadow.offset
readonly property int shadowRange: Hypr.options.decoration.shadow.range readonly property int shadowRange: Hypr.options.decoration.shadow.range
@@ -97,10 +98,6 @@ Item {
const args = Config.screenshot.mode === "auto" ? ["--rounding", `${rounding}`, "--radius", root.cornerRadius, "--shadow", root.shadowEnabled, "--shadow-blur", `${shadow_blur}`, "--shadow-color", `${r},${g},${b},${a}`, "--shadow-offset-x", root.shadowOffset[0], "--shadow-offset-y", root.shadowOffset[1]] : []; const args = Config.screenshot.mode === "auto" ? ["--rounding", `${rounding}`, "--radius", root.cornerRadius, "--shadow", root.shadowEnabled, "--shadow-blur", `${shadow_blur}`, "--shadow-color", `${r},${g},${b},${a}`, "--shadow-offset-x", root.shadowOffset[0], "--shadow-offset-y", root.shadowOffset[1]] : [];
const postCmd = Config.screenshot.enable_pp ? ["zshell-img-tools", "--scale", root.scaleRatio, ...args, "--image"] : null; const postCmd = Config.screenshot.enable_pp ? ["zshell-img-tools", "--scale", root.scaleRatio, ...args, "--image"] : null;
overlay.visible = border.visible = false;
handles.visible = false;
toolbar.visible = false;
ZShellIo.saveItem(captureLayer, tmpfile, Qt.rect(Math.ceil(rsx), Math.ceil(rsy), Math.floor(sw), Math.floor(sh)), path => { ZShellIo.saveItem(captureLayer, tmpfile, Qt.rect(Math.ceil(rsx), Math.ceil(rsy), Math.floor(sw), Math.floor(sh)), path => {
const finalize = finalPath => { const finalize = finalPath => {
if (copyToClipboard) { if (copyToClipboard) {
@@ -126,39 +123,35 @@ Item {
focus: true focus: true
opacity: 0 opacity: 0
Behavior on rsx {
enabled: !selectArea.pressed && root.mode === "select"
Anim {}
}
Behavior on rsy {
enabled: !selectArea.pressed && root.mode === "select"
Anim {}
}
Behavior on sh {
enabled: !selectArea.pressed && root.mode === "select"
Anim {}
}
Behavior on sw {
enabled: !selectArea.pressed && root.mode === "select"
Anim {}
}
Behavior on opacity { Behavior on opacity {
Anim { Anim {
duration: 300 duration: 300
} }
} }
Behavior on rsx {
enabled: !selectArea.pressed && root.mode === "select"
ExAnim {
}
}
Behavior on rsy {
enabled: !selectArea.pressed && root.mode === "select"
ExAnim {
}
}
Behavior on sh {
enabled: !selectArea.pressed && root.mode === "select"
ExAnim {
}
}
Behavior on sw {
enabled: !selectArea.pressed && root.mode === "select"
ExAnim {
}
}
Component.onCompleted: { Component.onCompleted: {
Hypr.extras.refreshOptions(); Hypr.extras.refreshOptions();
screencopy.active = true; root.forceActiveFocus();
opacity = 1; opacity = 1;
@@ -223,13 +216,63 @@ Item {
onReleased: root.enterEditMode() onReleased: root.enterEditMode()
} }
Connections {
function onClosingChanged(): void {
if (root.loader.closing)
closeAnim.start();
else {
closeAnim.stop();
returnAnim.start();
}
}
target: root.loader
}
ParallelAnimation {
id: returnAnim
Anim {
duration: 300
property: "opacity"
target: root
to: 1
}
Anim {
property: "rsx"
target: root
to: Math.min(root.sx, root.ex)
}
Anim {
property: "rsy"
target: root
to: Math.min(root.sy, root.ey)
}
Anim {
property: "sw"
target: root
to: Math.abs(root.sx - root.ex)
}
Anim {
property: "sh"
target: root
to: Math.abs(root.sy - root.ey)
}
}
SequentialAnimation { SequentialAnimation {
id: closeAnim id: closeAnim
PropertyAction { ScriptAction {
property: "closing" script: {
target: root.loader const states = ShellState.allStates();
value: true for (const s of states)
s.screenshot = false;
}
} }
ParallelAnimation { ParallelAnimation {
@@ -240,19 +283,19 @@ Item {
to: 0 to: 0
} }
ExAnim { Anim {
properties: "rsx,rsy" properties: "rsx,rsy"
target: root target: root
to: 0 to: 0
} }
ExAnim { Anim {
property: "sw" property: "sw"
target: root target: root
to: root.screen.width to: root.screen.width
} }
ExAnim { Anim {
property: "sh" property: "sh"
target: root target: root
to: root.screen.height to: root.screen.height
@@ -271,17 +314,12 @@ Item {
anchors.fill: parent anchors.fill: parent
Loader { ScreencopyView {
id: screencopy id: screencopy
active: false
anchors.fill: parent anchors.fill: parent
asynchronous: true captureSource: root.screen
paintCursor: false
sourceComponent: ScreencopyView {
captureSource: root.screen
paintCursor: false
}
} }
AnnotationCanvas { AnnotationCanvas {
@@ -290,6 +328,7 @@ Item {
anchors.fill: parent anchors.fill: parent
inkColor: root.inkColor inkColor: root.inkColor
layer.enabled: true layer.enabled: true
selection: selectionRect
tool: root.mode === "edit" && root.tool !== "move" ? root.tool : "none" tool: root.mode === "edit" && root.tool !== "move" ? root.tool : "none"
layer.effect: MultiEffect { layer.effect: MultiEffect {
@@ -301,15 +340,14 @@ Item {
} }
} }
Rectangle { CustomRect {
id: overlay id: overlay
anchors.fill: parent anchors.fill: parent
color: "white" color: "white"
layer.enabled: true layer.enabled: true
opacity: 0.3 opacity: 0.3
radius: root.realRounding visible: screencopy.hasContent
visible: screencopy.item?.hasContent ?? false
layer.effect: MultiEffect { layer.effect: MultiEffect {
maskEnabled: true maskEnabled: true
@@ -338,39 +376,44 @@ Item {
} }
} }
Rectangle { CustomRect {
id: border id: border
border.color: Colors.palette.m3primary border.color: Colors.palette.m3primary
border.width: root.realBorderWidth border.width: root.realBorderWidth
color: "transparent"
implicitHeight: selectionRect.implicitHeight + root.realBorderWidth * 2 implicitHeight: selectionRect.implicitHeight + root.realBorderWidth * 2
implicitWidth: selectionRect.implicitWidth + root.realBorderWidth * 2 implicitWidth: selectionRect.implicitWidth + root.realBorderWidth * 2
radius: root.realRounding > 0 ? root.realRounding + root.realBorderWidth : 0 radius: root.realRounding > 0 ? root.realRounding + root.realBorderWidth : 0
visible: screencopy.item?.hasContent ?? false visible: screencopy.hasContent
x: selectionRect.x - root.realBorderWidth x: selectionRect.x - root.realBorderWidth
y: selectionRect.y - root.realBorderWidth y: selectionRect.y - root.realBorderWidth
Behavior on border.color { Behavior on border.color {
Anim { Anim {}
}
} }
} }
SelectionHandles { SelectionHandles {
id: handles id: handles
border: root.realBorderWidth
selH: root.sh selH: root.sh
selW: root.sw selW: root.sw
selX: root.rsx selX: root.rsx
selY: root.rsy selY: root.rsy
selection: selectionRect
tool: root.tool
visible: root.mode === "edit" && root.tool === "move" visible: root.mode === "edit" && root.tool === "move"
onBoundsChanged: (x, y, w, h) => { onBoundsChanged: (x, y, w, h) => {
root.sx = x; let nx = Math.max(0, x);
root.sy = y; let ny = Math.max(0, y);
root.ex = x + w; let nw = Math.min(w, root.screen.width - nx);
root.ey = y + h; let nh = Math.min(h, root.screen.height - ny);
root.sx = nx;
root.sy = ny;
root.ex = nx + nw;
root.ey = ny + nh;
} }
onMoveBy: (dx, dy) => { onMoveBy: (dx, dy) => {
const w = root.sw, h = root.sh; const w = root.sw, h = root.sh;
@@ -401,7 +444,4 @@ Item {
onToolSelected: t => root.tool = t onToolSelected: t => root.tool = t
onUndoRequested: annotations.undo() onUndoRequested: annotations.undo()
} }
component ExAnim: Anim {
}
} }
+51 -56
View File
@@ -8,13 +8,13 @@ Item {
property color inkColor: "#ff3b30" property color inkColor: "#ff3b30"
property real inkWidth: 4 property real inkWidth: 4
required property Rectangle selection
property string tool: "none" property string tool: "none"
signal changed signal changed
function clearAll() { function clearAll() {
committedCanvas.clear(); canvas.clear();
previewCanvas.clear();
changed(); changed();
} }
@@ -54,90 +54,85 @@ Item {
} }
function undo() { function undo() {
committedCanvas.undoLastGroup(); canvas.undoLastGroup();
changed(); changed();
} }
anchors.fill: parent anchors.fill: parent
enabled: tool !== "none"
StrokeCanvas { StrokeCanvas {
id: committedCanvas id: canvas
anchors.fill: parent anchors.fill: parent
penColor: root.inkColor penColor: root.inkColor
penWidth: root.inkWidth penWidth: root.inkWidth
} }
StrokeCanvas { MouseArea {
id: previewCanvas
anchors.fill: parent
penColor: root.inkColor
penWidth: root.inkWidth
}
PointHandler {
id: drawHandler id: drawHandler
property real startX property real startX
property real startY property real startY
property bool started: false
acceptedButtons: Qt.LeftButton | Qt.RightButton acceptedButtons: Qt.LeftButton | Qt.RightButton
enabled: root.tool !== "none" anchors.fill: parent
cursorShape: undefined
enabled: root.enabled
onActiveChanged: { onPositionChanged: e => {
if (active) const x = e.x;
const y = e.y;
if (!pressed)
return; return;
if (root.tool === "pen") { if (root.tool === "pen") {
committedCanvas.endStroke(); canvas.appendPoint(x, y);
} else if (started) {
const pts = root.sampleShape(root.tool, startX, startY, point.position.x, point.position.y);
if (pts.length > 0) {
committedCanvas.beginStroke(pts[0][0], pts[0][1], true);
for (let i = 1; i < pts.length; i++)
committedCanvas.appendPoint(pts[i][0], pts[i][1]);
committedCanvas.endStroke();
}
previewCanvas.clear();
}
started = false;
root.changed();
}
onPointChanged: {
if (!active)
return; return;
}
if (point.pressedButtons & Qt.RightButton) { const pts = root.sampleShape(root.tool, startX, startY, x, y);
root.clearAll(); if (pts.length > 0) {
return; canvas.beginStroke(pts[0][0], pts[0][1], true);
for (let i = 1; i < pts.length; i++)
canvas.appendPoint(pts[i][0], pts[i][1]);
} }
}
onPressed: e => {
if (e.button & Qt.LeftButton) {
startX = e.x;
startY = e.y;
canvas.beginStroke(e.x, e.y);
} else if (e.button & Qt.RightButton) {
root.clearAll();
}
}
onPressedChanged: if (!pressed)
canvas.endStroke()
}
HoverHandler {
cursorShape: {
if (root.tool === "none")
return undefined;
const x = point.position.x; const x = point.position.x;
const y = point.position.y; const y = point.position.y;
const withinXWidth = x > root.selection.x && x < root.selection.x + root.selection.implicitWidth;
const withinYHeight = y > root.selection.y && y < root.selection.y + root.selection.implicitHeight;
if (root.tool === "pen") { if (withinXWidth && withinYHeight) {
if (!started) { if (!drawHandler.pressed)
started = true; canvas.showHover(x, y);
committedCanvas.beginStroke(x, y); else
return; canvas.showHover(drawHandler.mouseX, drawHandler.mouseY);
} return Qt.BlankCursor;
committedCanvas.appendPoint(x, y); } else {
return; canvas.hideHover();
} return undefined;
if (!started) {
started = true;
startX = point.pressPosition.x;
startY = point.pressPosition.y;
}
const pts = root.sampleShape(root.tool, startX, startY, x, y);
if (pts.length > 0) {
previewCanvas.beginStroke(pts[0][0], pts[0][1], true);
for (let i = 1; i < pts.length; i++)
previewCanvas.appendPoint(pts[i][0], pts[i][1]);
} }
} }
enabled: root.enabled
} }
} }
+7 -7
View File
@@ -73,7 +73,7 @@ CustomRect {
required property var modelData required property var modelData
color: root.tool === modelData.id ? "#3584e4" : "transparent" color: root.tool === modelData.id ? "#3584e4" : "transparent"
font.pointSize: Tokens.font.size.large font: Tokens.font.icon.medium
icon: tool.modelData.glyph icon: tool.modelData.glyph
inactiveColor: root.tool === modelData.id ? Colors.palette.m3primary : "transparent" inactiveColor: root.tool === modelData.id ? Colors.palette.m3primary : "transparent"
inactiveOnColor: root.tool === modelData.id ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface inactiveOnColor: root.tool === modelData.id ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
@@ -103,7 +103,7 @@ CustomRect {
required property color modelData required property color modelData
fillWidth: false fillWidth: false
font.pointSize: Tokens.font.size.large font: Tokens.font.icon.medium
icon: "" icon: ""
implicitHeight: buttonSize implicitHeight: buttonSize
implicitWidth: buttonSize implicitWidth: buttonSize
@@ -128,7 +128,7 @@ CustomRect {
spacing: Tokens.spacing.extraSmall spacing: Tokens.spacing.extraSmall
IconButton { IconButton {
font.pointSize: Tokens.font.size.large font: Tokens.font.icon.medium
icon: "undo" icon: "undo"
inactiveColor: "transparent" inactiveColor: "transparent"
inactiveOnColor: Colors.palette.m3onSurface inactiveOnColor: Colors.palette.m3onSurface
@@ -140,7 +140,7 @@ CustomRect {
} }
IconButton { IconButton {
font.pointSize: Tokens.font.size.large font: Tokens.font.icon.medium
icon: "delete_history" icon: "delete_history"
inactiveColor: "transparent" inactiveColor: "transparent"
inactiveOnColor: Colors.palette.m3onSurface inactiveOnColor: Colors.palette.m3onSurface
@@ -152,7 +152,7 @@ CustomRect {
} }
IconButton { IconButton {
font.pointSize: Tokens.font.size.large font: Tokens.font.icon.medium
icon: "content_copy" icon: "content_copy"
inactiveColor: "transparent" inactiveColor: "transparent"
inactiveOnColor: Colors.palette.m3onSurface inactiveOnColor: Colors.palette.m3onSurface
@@ -164,7 +164,7 @@ CustomRect {
} }
IconButton { IconButton {
font.pointSize: Tokens.font.size.large font: Tokens.font.icon.medium
icon: "delete_forever" icon: "delete_forever"
inactiveColor: "transparent" inactiveColor: "transparent"
inactiveOnColor: Colors.palette.m3onSurface inactiveOnColor: Colors.palette.m3onSurface
@@ -176,7 +176,7 @@ CustomRect {
} }
IconButton { IconButton {
font.pointSize: Tokens.font.size.large font: Tokens.font.icon.medium
icon: "check" icon: "check"
inactiveColor: "transparent" inactiveColor: "transparent"
inactiveOnColor: Colors.palette.m3onSurface inactiveOnColor: Colors.palette.m3onSurface
+40 -14
View File
@@ -1,18 +1,23 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import ZShell.Internal
import qs.Services
Item { Item {
id: root id: root
property color handleBorder: "#3584e4" required property int border
property color handleFill: "white" property color handleBorder: Colors.palette.m3primary
property color handleFill: Colors.palette.m3onPrimary
property real hs: 12 property real hs: 12
property real minSize: 24 property real minSize: 24
required property real selH required property real selH
required property real selW required property real selW
required property real selX required property real selX
required property real selY required property real selY
required property Rectangle selection
required property string tool
signal boundsChanged(x: real, y: real, w: real, h: real) signal boundsChanged(x: real, y: real, w: real, h: real)
signal dragFinished signal dragFinished
@@ -23,12 +28,16 @@ Item {
x: selX x: selX
y: selY y: selY
Component.onDestruction: Cursor.clearOverride()
DragHandler { DragHandler {
id: moveHandler id: moveHandler
property real accumX: 0 property real accumX: 0
property real accumY: 0 property real accumY: 0
cursorShape: undefined
dragThreshold: 0
target: null target: null
onActiveChanged: { onActiveChanged: {
@@ -45,51 +54,63 @@ Item {
} }
} }
HoverHandler {
cursorShape: {
if (root.tool !== "move")
return undefined;
if ((moveHandler.centroid.pressedButtons & Qt.LeftButton) && moveHandler.active)
return Qt.ClosedHandCursor;
else
return Qt.OpenHandCursor;
}
}
Handle { Handle {
edge: "tl" edge: "tl"
x: -root.hs / 2 x: -(root.hs + root.border) / 2
y: -root.hs / 2 y: -(root.hs + root.border) / 2
} }
Handle { Handle {
edge: "t" edge: "t"
x: root.width / 2 - root.hs / 2 x: root.width / 2 - root.hs / 2
y: -root.hs / 2 y: -(root.hs + root.border) / 2
} }
Handle { Handle {
edge: "tr" edge: "tr"
x: root.width - root.hs / 2 x: root.width - root.hs / 2 + root.border / 2
y: -root.hs / 2 y: -(root.hs + root.border) / 2
} }
Handle { Handle {
edge: "r" edge: "r"
x: root.width - root.hs / 2 x: root.width - root.hs / 2 + root.border / 2
y: root.height / 2 - root.hs / 2 y: root.height / 2 - root.hs / 2
} }
Handle { Handle {
edge: "br" edge: "br"
x: root.width - root.hs / 2 x: root.width - root.hs / 2 + root.border / 2
y: root.height - root.hs / 2 y: root.height - root.hs / 2 + root.border / 2
} }
Handle { Handle {
edge: "b" edge: "b"
x: root.width / 2 - root.hs / 2 x: root.width / 2 - root.hs / 2
y: root.height - root.hs / 2 y: root.height - root.hs / 2 + root.border / 2
} }
Handle { Handle {
edge: "bl" edge: "bl"
x: -root.hs / 2 x: -(root.hs + root.border) / 2
y: root.height - root.hs / 2 y: root.height - root.hs / 2 + root.border / 2
} }
Handle { Handle {
edge: "l" edge: "l"
x: -root.hs / 2 x: -(root.hs + root.border) / 2
y: root.height / 2 - root.hs / 2 y: root.height / 2 - root.hs / 2
} }
@@ -107,6 +128,8 @@ Item {
z: 10 z: 10
HoverHandler { HoverHandler {
id: hover
cursorShape: { cursorShape: {
switch (handle.edge) { switch (handle.edge) {
case "tl": case "tl":
@@ -132,6 +155,7 @@ Item {
property real startX property real startX
property real startY property real startY
dragThreshold: 0
grabPermissions: PointerHandler.CanTakeOverFromAnything | PointerHandler.TakeOverForbidden grabPermissions: PointerHandler.CanTakeOverFromAnything | PointerHandler.TakeOverForbidden
target: null target: null
@@ -141,8 +165,10 @@ Item {
startY = root.y; startY = root.y;
startW = root.width; startW = root.width;
startH = root.height; startH = root.height;
Cursor.setOverride(hover.cursorShape);
} else { } else {
root.dragFinished(); root.dragFinished();
Cursor.clearOverride();
} }
} }
onTranslationChanged: { onTranslationChanged: {
+9
View File
@@ -5,6 +5,15 @@ import Quickshell
Singleton { Singleton {
property var _regexCache: ({}) property var _regexCache: ({})
function percent(value: int): string {
// TRANSLATORS: %1 = a number
return qsTr("%1%").arg(value);
}
function percentOne(value: real): string {
return percent(Math.round(value * 100));
}
function testRegexList(filterList: list<string>, target: string): bool { function testRegexList(filterList: list<string>, target: string): bool {
const regexChecker = /^\^.*\$$/; const regexChecker = /^\^.*\$$/;
for (const filter of filterList) { for (const filter of filterList) {
-16
View File
@@ -1,16 +0,0 @@
pragma Singleton
import Quickshell
Singleton {
property var bars: new Map()
property var screens: new Map()
function getForActive(): PersistentProperties {
return screens.get(Hypr.focusedMonitor);
}
function load(screen: ShellScreen, visibilities: var): void {
screens.set(Hypr.monitorFor(screen), visibilities);
}
}
+41 -3
View File
@@ -5,7 +5,9 @@ import Quickshell
import Quickshell.Io import Quickshell.Io
import QtQuick import QtQuick
import ZShell.Models import ZShell.Models
import ZShell.Internal
import ZShell.Config import ZShell.Config
import qs.Components
import qs.Services import qs.Services
import qs.Helpers import qs.Helpers
import qs.Paths import qs.Paths
@@ -38,7 +40,7 @@ Searcher {
previewPath = path; previewPath = path;
showPreview = true; showPreview = true;
if (Config.general.color.schemeGeneration) if (Config.colors.schemeGen)
previewColorsProc.running = true; previewColorsProc.running = true;
} }
@@ -62,8 +64,8 @@ Searcher {
WallpaperPath.currentWallpaperPath = path; WallpaperPath.currentWallpaperPath = path;
Quickshell.screens.forEach(n => setCrop(n.name, Qt.rect(0, 0, 1, 1), 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}`]); 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) if (Config.colors.schemeGen)
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--image-path", `${root.actualCurrent}`, "--scheme", `${Config.colors.schemeType}`, "--mode", `${Config.general.color.mode}`]); Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--image-path", `${root.actualCurrent}`, "--scheme", `${Config.colors.schemeType}`, "--mode", `${Config.colors.mode}`]);
} }
function stopPreview(): void { function stopPreview(): void {
@@ -140,4 +142,40 @@ Searcher {
} }
} }
} }
WallpaperService {
id: wallpaper
path: root.current
}
Variants {
model: Quickshell.screens
WallpaperObject {
readonly property var crops: root.getCrop(modelData.name)
required property var modelData
cropHeight: crops?.height ?? 1.0
cropWidth: crops?.width ?? 1.0
cropX: crops?.x ?? 0.0
cropY: crops?.y ?? 0.0
screen: modelData.name
service: wallpaper
zoom: crops?.zoom ?? 1.0
Behavior on cropHeight {
Anim {}
}
Behavior on cropWidth {
Anim {}
}
Behavior on cropX {
Anim {}
}
Behavior on cropY {
Anim {}
}
}
}
} }
+28 -7
View File
@@ -3,25 +3,36 @@ pragma Singleton
import QtQuick import QtQuick
import Quickshell import Quickshell
import ZShell import ZShell
import ZShell.Internal
import ZShell.Config import ZShell.Config
Singleton { Singleton {
id: root id: root
readonly property var cachedCities: new Map() readonly property var cachedCities: new Map()
property var cc
property string city
readonly property string description: cc?.weatherDesc ?? qsTr("No weather") readonly property string description: cc?.weatherDesc ?? qsTr("No weather")
readonly property string feelsLike: formatTemp(cc?.feelsLikeC) readonly property string feelsLike: formatTemp(cc?.feelsLikeC)
property list<var> forecast
property list<var> hourlyForecast
readonly property int humidity: cc?.humidity ?? 0 readonly property int humidity: cc?.humidity ?? 0
readonly property string icon: cc ? Icons.getWeatherIcon(cc.weatherCode) : "cloud_alert" readonly property string icon: cc ? Icons.getWeatherIcon(cc.weatherCode) : "cloud_alert"
property string loc
readonly property string sunrise: cc ? Qt.formatDateTime(new Date(cc.sunrise), Config.services.useTwelveHourClock ? "h:mm A" : "h:mm") : "--:--" readonly property string sunrise: cc ? Qt.formatDateTime(new Date(cc.sunrise), Config.services.useTwelveHourClock ? "h:mm A" : "h:mm") : "--:--"
readonly property string sunset: cc ? Qt.formatDateTime(new Date(cc.sunset), Config.services.useTwelveHourClock ? "h:mm A" : "h:mm") : "--:--" readonly property string sunset: cc ? Qt.formatDateTime(new Date(cc.sunset), Config.services.useTwelveHourClock ? "h:mm A" : "h:mm") : "--:--"
readonly property string temp: formatTemp(cc?.tempC) readonly property string temp: formatTemp(cc?.tempC)
readonly property real windSpeed: cc?.windSpeed ?? 0 readonly property real windSpeed: cc?.windSpeed ?? 0
readonly property CircularBuffer precipitationForecast: precipitationBuffer
property var cc
property string city
property list<var> forecast
property list<var> hourlyForecast
readonly property int currentHourIndex: {
const now = new Date();
for (let i = 0; i < hourlyForecast.length; i++) {
if (new Date(hourlyForecast[i].timestamp.replace("T", " ")) >= now)
return i;
}
return -1;
}
property string loc
function fetchCityFromCoords(coords: string): void { function fetchCityFromCoords(coords: string): void {
if (cachedCities.has(coords)) { if (cachedCities.has(coords)) {
@@ -112,11 +123,12 @@ Singleton {
forecast = forecastList; forecast = forecastList;
const hourlyList = []; const hourlyList = [];
const now = new Date(); const startOfToday = new Date();
startOfToday.setHours(0, 0, 0, 0);
for (let i = 0; i < json.hourly.time.length; i++) { for (let i = 0; i < json.hourly.time.length; i++) {
const time = new Date(json.hourly.time[i].replace("T", " ")); const time = new Date(json.hourly.time[i].replace("T", " "));
if (time < now) if (time < startOfToday)
continue; continue;
hourlyList.push({ hourlyList.push({
@@ -129,6 +141,10 @@ Singleton {
}); });
} }
hourlyForecast = hourlyList; hourlyForecast = hourlyList;
precipitationBuffer.clear();
for (const hour of hourlyList)
precipitationBuffer.push(hour.precipChance);
}); });
} }
@@ -258,6 +274,11 @@ Singleton {
onLocChanged: fetchWeatherData() onLocChanged: fetchWeatherData()
CircularBuffer {
id: precipitationBuffer
capacity: 24
}
Connections { Connections {
function onWeatherLocationChanged(): void { function onWeatherLocationChanged(): void {
root.reload(); root.reload();
+9 -8
View File
@@ -4,6 +4,7 @@ import Quickshell
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Config import ZShell.Config
import qs.Components
import qs.Modules.Bar.Components import qs.Modules.Bar.Components
import qs.Modules.Bar.Popouts import qs.Modules.Bar.Popouts
@@ -16,12 +17,12 @@ GridLayout {
required property Wrapper popouts required property Wrapper popouts
required property ClipWrapper popoutsWrapper required property ClipWrapper popoutsWrapper
required property ShellScreen screen required property ShellScreen screen
required property ScreenState screenState
readonly property int vPadding: 6 readonly property int vPadding: 6
required property PersistentProperties visibilities
function axisCenterOf(item: Item): real { function axisCenterOf(item: Item): real {
const c = item.mapToItem(root, item.implicitWidth / 2, item.implicitHeight / 2); const c = item.mapToItem(root, item.implicitWidth / 2, item.implicitHeight / 2);
return horizontal ? c.x : c.y; return horizontal ? Math.floor(c.x) : Math.floor(c.y);
} }
function checkPopout(pos: real): void { function checkPopout(pos: real): void {
@@ -34,7 +35,7 @@ GridLayout {
return; return;
} }
if (visibilities.sidebar || visibilities.dashboard || visibilities.resources || visibilities.settings) if (screenState.sidebar || screenState.dashboard || screenState.resources || screenState.settings)
return; return;
if (id === "statusIcons" && Config.bar.popouts.statusIcons) { if (id === "statusIcons" && Config.bar.popouts.statusIcons) {
@@ -88,10 +89,10 @@ GridLayout {
role: "id" role: "id"
DelegateChoice { DelegateChoice {
roleValue: "hyprsunset" roleValue: "nightlight"
delegate: EntryWrapper { delegate: EntryWrapper {
HyprsunsetWidget { NightlightWidget {
horizontal: root.horizontal horizontal: root.horizontal
visible: !root.fullscreen || root.isHovered visible: !root.fullscreen || root.isHovered
} }
@@ -149,7 +150,7 @@ GridLayout {
delegate: EntryWrapper { delegate: EntryWrapper {
Resources { Resources {
horizontal: root.horizontal horizontal: root.horizontal
visibilities: root.visibilities screenState: root.screenState
visible: !root.fullscreen || root.isHovered visible: !root.fullscreen || root.isHovered
} }
} }
@@ -173,7 +174,7 @@ GridLayout {
NotifBell { NotifBell {
horizontal: root.horizontal horizontal: root.horizontal
popouts: root.popouts popouts: root.popouts
visibilities: root.visibilities screenState: root.screenState
visible: !root.fullscreen || root.isHovered visible: !root.fullscreen || root.isHovered
} }
} }
@@ -187,7 +188,7 @@ GridLayout {
horizontal: root.horizontal horizontal: root.horizontal
loader: root loader: root
popouts: root.popouts popouts: root.popouts
visibilities: root.visibilities screenState: root.screenState
visible: !root.fullscreen || root.isHovered visible: !root.fullscreen || root.isHovered
} }
} }
+4 -4
View File
@@ -10,7 +10,7 @@ Item {
id: root id: root
readonly property int clampedExtent: Math.max(Config.bar.border, extent) readonly property int clampedExtent: Math.max(Config.bar.border, extent)
readonly property int contentThickness: Math.floor(Math.max(Config.bar.height, Tokens.bar.innerSize * (horizontal ? 1 : 1.5))) + padding * 2 readonly property int contentThickness: Math.floor(Math.max(Config.bar.height, Tokens.sizes.bar.innerSize * (horizontal ? 1 : 1.5))) + padding * 2
readonly property int exclusiveZone: Config.bar.autoHide ? Config.bar.border : contentThickness readonly property int exclusiveZone: Config.bar.autoHide ? Config.bar.border : contentThickness
property real extent: fullscreen ? 0 : Config.bar.border property real extent: fullscreen ? 0 : Config.bar.border
required property bool fullscreen required property bool fullscreen
@@ -21,9 +21,9 @@ Item {
required property ClipWrapper popoutsWrapper required property ClipWrapper popoutsWrapper
required property string position required property string position
required property ShellScreen screen required property ShellScreen screen
readonly property bool shouldBeVisible: (!fullscreen && (!Config.bar.autoHide || visibilities.bar)) || isHovered readonly property bool shouldBeVisible: (!fullscreen && (!Config.bar.autoHide || screenState.bar)) || isHovered
readonly property int vPadding: 6 readonly property int vPadding: 6
required property PersistentProperties visibilities required property ScreenState screenState
function checkPopout(pos: real): void { function checkPopout(pos: real): void {
(content.item as Bar)?.checkPopout(pos); (content.item as Bar)?.checkPopout(pos);
@@ -85,7 +85,7 @@ Item {
popouts: root.popouts popouts: root.popouts
popoutsWrapper: root.popoutsWrapper popoutsWrapper: root.popoutsWrapper
screen: root.screen screen: root.screen
visibilities: root.visibilities screenState: root.screenState
} }
} }
} }
+1 -1
View File
@@ -11,7 +11,7 @@ Item {
id: root id: root
required property Item bar required property Item bar
required property PersistentProperties visibilities required property ScreenState screenState
anchors.fill: parent anchors.fill: parent
+12 -25
View File
@@ -13,7 +13,8 @@ import qs.Services
WidgetBase { WidgetBase {
id: root id: root
readonly property color contentColor: visibilities.dashboard ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface readonly property font font: Tokens.font.clock.family(Config.appearance.font.family.clock).build()
readonly property color contentColor: Colors.palette.m3onSurface
readonly property string effectiveFormat: { readonly property string effectiveFormat: {
let fmt = Config.general.dateFormat; let fmt = Config.general.dateFormat;
if (!root.horizontal) if (!root.horizontal)
@@ -44,10 +45,9 @@ WidgetBase {
} }
required property GridLayout loader required property GridLayout loader
required property Wrapper popouts required property Wrapper popouts
readonly property real size: horizontal ? timeText.contentWidth + Tokens.padding.small * 2 : verticalColumn.implicitHeight + Tokens.padding.small * 2 readonly property real size: horizontal ? timeText.contentWidth + Tokens.padding.medium * 2 : verticalColumn.implicitHeight + Tokens.padding.medium * 2
required property PersistentProperties visibilities required property ScreenState screenState
color: visibilities.dashboard ? Colors.palette.m3primary : Colors.tPalette.m3surfaceContainer
implicitHeight: horizontal ? baseSize : size implicitHeight: horizontal ? baseSize : size
implicitWidth: horizontal ? size : baseSize implicitWidth: horizontal ? size : baseSize
radius: Tokens.rounding.full radius: Tokens.rounding.full
@@ -57,14 +57,13 @@ WidgetBase {
anchors.centerIn: parent anchors.centerIn: parent
color: root.contentColor color: root.contentColor
font.family: Config.appearance.font.family.mono // qmllint disable incompatible-type font: root.font
height: implicitHeight height: implicitHeight
text: Time.dateStr text: Time.dateStr
visible: root.horizontal visible: root.horizontal
Behavior on color { Behavior on color {
CAnim { CAnim {}
}
} }
} }
@@ -84,12 +83,11 @@ WidgetBase {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
color: root.contentColor color: root.contentColor
font.family: Config.appearance.font.family.mono // qmllint disable incompatible-type font: root.font
text: Qt.formatDateTime(Time.date, modelData) text: Qt.formatDateTime(Time.date, modelData)
Behavior on color { Behavior on color {
CAnim { CAnim {}
}
} }
} }
} }
@@ -114,7 +112,7 @@ WidgetBase {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
color: root.contentColor color: root.contentColor
font.family: Config.appearance.font.family.mono // qmllint disable incompatible-type font: root.font
text: { text: {
if (modelData.includes("h")) if (modelData.includes("h"))
return Time.hourStr; return Time.hourStr;
@@ -126,8 +124,7 @@ WidgetBase {
} }
Behavior on color { Behavior on color {
CAnim { CAnim {}
}
} }
} }
} }
@@ -135,23 +132,13 @@ WidgetBase {
CustomText { CustomText {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
color: root.contentColor color: root.contentColor
font.family: Config.appearance.font.family.mono // qmllint disable incompatible-type font: root.font
text: Qt.formatDateTime(Time.date, "AP") text: Qt.formatDateTime(Time.date, "AP")
visible: Config.services.useTwelveHourClock && root.formatParts.timeTokens.length > 0 visible: Config.services.useTwelveHourClock && root.formatParts.timeTokens.length > 0
Behavior on color { Behavior on color {
CAnim { CAnim {}
}
} }
} }
} }
StateLayer {
acceptedButtons: Qt.LeftButton
color: root.contentColor
onClicked: {
root.visibilities.dashboard = !root.visibilities.dashboard;
}
}
} }
@@ -28,8 +28,7 @@ Row {
CustomText { CustomText {
color: Battery.getColors(root.percentage, root.devState).fg color: Battery.getColors(root.percentage, root.devState).fg
font.pointSize: Tokens.font.size.larger / 1.5 font: Tokens.font.body.builders.small.size(11).weight(Font.Bold).build()
font.weight: 800
height: track.height height: track.height
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
text: Math.round(root.percentage * 100) text: Math.round(root.percentage * 100)
@@ -60,8 +59,7 @@ Row {
clip: true clip: true
color: Battery.getColors(root.percentage, root.devState).bg color: Battery.getColors(root.percentage, root.devState).bg
font.pointSize: Tokens.font.size.larger / 1.5 font: Tokens.font.body.builders.small.size(11).weight(Font.Bold).build()
font.weight: 800
height: track.height height: track.height
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
text: Math.round(root.percentage * 100) text: Math.round(root.percentage * 100)
+1 -1
View File
@@ -6,7 +6,7 @@ import qs.Services
CustomRect { CustomRect {
id: root id: root
readonly property int baseSize: horizontal ? Math.max(Config.bar.height, Tokens.bar.innerSize) : Math.max(Config.bar.height, Math.floor(Tokens.bar.innerSize * 1.5)) readonly property int baseSize: horizontal ? Math.max(Config.bar.height, Tokens.sizes.bar.innerSize) : Math.max(Config.bar.height, Math.floor(Tokens.sizes.bar.innerSize * 1.5))
required property bool horizontal required property bool horizontal
color: Colors.tPalette.m3surfaceContainer color: Colors.tPalette.m3surfaceContainer
+4 -8
View File
@@ -19,14 +19,12 @@ WidgetBase {
Behavior on implicitHeight { Behavior on implicitHeight {
enabled: !root.horizontal enabled: !root.horizontal
Anim { Anim {}
}
} }
Behavior on implicitWidth { Behavior on implicitWidth {
enabled: root.horizontal enabled: root.horizontal
Anim { Anim {}
}
} }
TextMetrics { TextMetrics {
@@ -47,15 +45,14 @@ WidgetBase {
columns: root.horizontal ? -1 : 1 columns: root.horizontal ? -1 : 1
Behavior on implicitWidth { Behavior on implicitWidth {
Anim { Anim {}
}
} }
MaterialIcon { MaterialIcon {
Layout.alignment: root.horizontal ? Qt.AlignVCenter : Qt.AlignHCenter Layout.alignment: root.horizontal ? Qt.AlignVCenter : Qt.AlignHCenter
animate: true animate: true
color: Players.active?.isPlaying ? Colors.palette.m3primary : Colors.palette.m3onSurface color: Players.active?.isPlaying ? Colors.palette.m3primary : Colors.palette.m3onSurface
font.pointSize: Tokens.font.size.larger fontStyle: Tokens.font.icon.small
text: Players.active?.isPlaying ? "music_note" : "music_off" text: Players.active?.isPlaying ? "music_note" : "music_off"
} }
@@ -65,7 +62,6 @@ WidgetBase {
Layout.alignment: root.horizontal ? Qt.AlignVCenter : Qt.AlignHCenter Layout.alignment: root.horizontal ? Qt.AlignVCenter : Qt.AlignHCenter
animate: true animate: true
color: Players.active?.isPlaying ? Colors.palette.m3primary : Colors.palette.m3onSurface color: Players.active?.isPlaying ? Colors.palette.m3primary : Colors.palette.m3onSurface
font.pointSize: Tokens.font.size.normal
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
implicitHeight: root.horizontal ? root.implicitHeight : root.textWidth implicitHeight: root.horizontal ? root.implicitHeight : root.textWidth
implicitWidth: root.horizontal ? root.textWidth : root.implicitWidth implicitWidth: root.horizontal ? root.textWidth : root.implicitWidth
@@ -8,15 +8,15 @@ import qs.Services
WidgetBase { WidgetBase {
id: root id: root
property bool tempEnabled: Hyprsunset.enabled property bool tempEnabled: Nightlight.enabled
color: root.tempEnabled ? Colors.palette.m3primary : Colors.tPalette.m3surfaceContainer color: root.tempEnabled ? Colors.palette.m3primary : Colors.tPalette.m3surfaceContainer
radius: Tokens.rounding.full radius: Tokens.rounding.full
StateLayer { StateLayer {
onClicked: { onClicked: {
Hyprsunset.toggleNightLight(); Nightlight.toggleNightLight();
Hyprsunset.manualToggle = true; Nightlight.manualToggle = true;
} }
} }
@@ -25,12 +25,11 @@ WidgetBase {
animate: true animate: true
color: root.tempEnabled ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface color: root.tempEnabled ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
fill: root.tempEnabled ? 1 : 0 fill: root.tempEnabled ? 1 : 0
font.pointSize: Tokens.font.size.larger fontStyle: Tokens.font.icon.small
text: root.tempEnabled ? "lightbulb" : "light_off" text: root.tempEnabled ? "lightbulb" : "light_off"
Behavior on fill { Behavior on fill {
Anim { Anim {}
}
} }
} }
} }
+9 -11
View File
@@ -11,35 +11,33 @@ WidgetBase {
id: root id: root
required property Wrapper popouts required property Wrapper popouts
required property PersistentProperties visibilities required property ScreenState screenState
color: visibilities.sidebar ? Colors.palette.m3primary : Colors.tPalette.m3surfaceContainer color: screenState.sidebar ? Colors.palette.m3primary : Colors.tPalette.m3surfaceContainer
radius: Tokens.rounding.full radius: Tokens.rounding.full
MaterialIcon { MaterialIcon {
id: notificationCenterIcon id: notificationCenterIcon
anchors.centerIn: parent anchors.centerIn: parent
color: root.visibilities.sidebar ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface color: root.screenState.sidebar ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
fill: root.visibilities.sidebar ? 1 : 0 fill: root.screenState.sidebar ? 1 : 0
font.pointSize: Tokens.font.size.larger fontStyle: Tokens.font.icon.size(16).build()
text: NotifServer.list.length ? "\uf4fe" : "\ue7f4" text: NotifServer.list.length ? "\uf4fe" : "\ue7f4"
Behavior on color { Behavior on color {
CAnim { CAnim {}
}
} }
Behavior on fill { Behavior on fill {
Anim { Anim {}
}
} }
} }
StateLayer { StateLayer {
color: root.visibilities.sidebar ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface color: root.screenState.sidebar ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
onClicked: { onClicked: {
root.visibilities.sidebar = !root.visibilities.sidebar; root.screenState.sidebar = !root.screenState.sidebar;
} }
} }
} }
+30 -17
View File
@@ -1,6 +1,6 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Shapes import Quickshell
import qs.Components import qs.Components
import ZShell.Config import ZShell.Config
import qs.Services import qs.Services
@@ -12,6 +12,7 @@ GridLayout {
property real animatedPercentage: 0 property real animatedPercentage: 0
readonly property real arcStartAngle: 0.75 * Math.PI readonly property real arcStartAngle: 0.75 * Math.PI
readonly property real arcSweep: 1.5 * Math.PI readonly property real arcSweep: 1.5 * Math.PI
readonly property real dpr: (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1
required property bool horizontal required property bool horizontal
property string icon property string icon
required property color iconColor required property color iconColor
@@ -22,6 +23,10 @@ GridLayout {
property bool warning: percentage * 100 >= warningThreshold property bool warning: percentage * 100 >= warningThreshold
property int warningThreshold: 80 property int warningThreshold: 80
function snap(v) {
return Math.round(v * dpr) / dpr;
}
columnSpacing: Tokens.spacing.medium columnSpacing: Tokens.spacing.medium
columns: horizontal ? -1 : 1 columns: horizontal ? -1 : 1
percentage: 0 percentage: 0
@@ -44,31 +49,39 @@ GridLayout {
Layout.preferredWidth: 16 Layout.preferredWidth: 16
color: root.iconColor color: root.iconColor
font.pointSize: Tokens.font.size.larger fontStyle: Tokens.font.icon.small
text: root.icon text: root.icon
} }
CustomClippingRect { CustomRect {
id: track
Layout.preferredHeight: root.horizontal ? icon.implicitHeight - Tokens.padding.extraSmall : 4 Layout.preferredHeight: root.horizontal ? icon.implicitHeight - Tokens.padding.extraSmall : 4
Layout.preferredWidth: root.horizontal ? 4 : icon.implicitWidth Layout.preferredWidth: root.horizontal ? 4 : icon.implicitWidth
color: Colors.layer(Colors.palette.m3surfaceContainerHigh, 2) color: Colors.layer(Colors.palette.m3surfaceContainerHigh, 2)
radius: Tokens.rounding.full radius: Math.min(width, height) / 2
CustomRect { Item {
id: fill id: clipper
anchors.bottom: parent.bottom anchors.bottom: root.horizontal ? parent.bottom : undefined
anchors.left: parent.left anchors.horizontalCenter: root.horizontal ? parent.horizontalCenter : undefined
anchors.right: root.horizontal ? parent.right : undefined anchors.left: root.horizontal ? undefined : parent.left
anchors.top: root.horizontal ? undefined : parent.top anchors.verticalCenter: root.horizontal ? undefined : parent.verticalCenter
color: root.mainColor clip: true
implicitHeight: root.horizontal ? Math.ceil(root.percentage * parent.height) : 0 height: root.horizontal ? root.snap(root.percentage * track.height) : track.height * 2
implicitWidth: root.horizontal ? 0 : Math.ceil(root.percentage * parent.width) width: root.horizontal ? track.width * 2 : root.snap(root.percentage * track.width)
// Behavior on implicitHeight { CustomRect {
// Anim { anchors.bottom: root.horizontal ? parent.bottom : undefined
// } anchors.horizontalCenter: root.horizontal ? parent.horizontalCenter : undefined
// } anchors.left: root.horizontal ? undefined : parent.left
anchors.verticalCenter: root.horizontal ? undefined : parent.verticalCenter
color: root.mainColor
height: track.height
radius: track.radius
width: track.width
}
} }
} }
} }
+12 -12
View File
@@ -12,18 +12,18 @@ import qs.Components
WidgetBase { WidgetBase {
id: root id: root
required property PersistentProperties visibilities required property ScreenState screenState
clip: true clip: true
color: visibilities.resources ? Colors.palette.m3primary : Colors.tPalette.m3surfaceContainer color: screenState.resources ? Colors.palette.m3primary : Colors.tPalette.m3surfaceContainer
implicitHeight: horizontal ? baseSize : gridLayout.implicitHeight + Tokens.padding.medium * 2 implicitHeight: horizontal ? baseSize : gridLayout.implicitHeight + Tokens.padding.medium * 2
implicitWidth: horizontal ? gridLayout.implicitWidth + Tokens.padding.medium * 2 : baseSize implicitWidth: horizontal ? gridLayout.implicitWidth + Tokens.padding.medium * 2 : baseSize
radius: Tokens.rounding.full radius: Tokens.rounding.full
StateLayer { StateLayer {
color: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface color: root.screenState.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
onClicked: root.visibilities.resources = !root.visibilities.resources onClicked: root.screenState.resources = !root.screenState.resources
} }
GridLayout { GridLayout {
@@ -53,8 +53,8 @@ WidgetBase {
Layout.fillWidth: !root.horizontal Layout.fillWidth: !root.horizontal
horizontal: root.horizontal horizontal: root.horizontal
icon: "memory" icon: "memory"
iconColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface iconColor: root.screenState.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
mainColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3primary mainColor: root.screenState.resources ? Colors.palette.m3onPrimary : Colors.palette.m3primary
percentage: Cpu.percentage percentage: Cpu.percentage
warningThreshold: 95 warningThreshold: 95
} }
@@ -64,8 +64,8 @@ WidgetBase {
Layout.fillWidth: !root.horizontal Layout.fillWidth: !root.horizontal
horizontal: root.horizontal horizontal: root.horizontal
icon: "memory_alt" icon: "memory_alt"
iconColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface iconColor: root.screenState.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
mainColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3secondary mainColor: root.screenState.resources ? Colors.palette.m3onPrimary : Colors.palette.m3secondary
percentage: Memory.percentage percentage: Memory.percentage
warningThreshold: 80 warningThreshold: 80
} }
@@ -75,8 +75,8 @@ WidgetBase {
Layout.fillWidth: !root.horizontal Layout.fillWidth: !root.horizontal
horizontal: root.horizontal horizontal: root.horizontal
icon: "gamepad" icon: "gamepad"
iconColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface iconColor: root.screenState.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
mainColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3tertiary mainColor: root.screenState.resources ? Colors.palette.m3onPrimary : Colors.palette.m3tertiary
percentage: Gpu.percentage percentage: Gpu.percentage
} }
@@ -85,8 +85,8 @@ WidgetBase {
Layout.fillWidth: !root.horizontal Layout.fillWidth: !root.horizontal
horizontal: root.horizontal horizontal: root.horizontal
icon: "developer_board" icon: "developer_board"
iconColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface iconColor: root.screenState.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
mainColor: root.visibilities.resources ? Colors.palette.m3onPrimary : Colors.palette.m3primary mainColor: root.screenState.resources ? Colors.palette.m3onPrimary : Colors.palette.m3primary
percentage: Gpu.memoryUsed / Gpu.memoryTotal percentage: Gpu.memoryUsed / Gpu.memoryTotal
} }
} }
@@ -13,19 +13,16 @@ MaterialIcon {
animate: true animate: true
color: Audio.muted ? Colors.palette.m3error : Colors.palette.m3onSurface color: Audio.muted ? Colors.palette.m3error : Colors.palette.m3onSurface
fill: 1 fill: 1
font.pointSize: Tokens.font.size.larger fontStyle: Tokens.font.icon.small
text: Audio.muted ? "volume_off" : "volume_up" text: Audio.muted ? "volume_off" : "volume_up"
Behavior on Layout.maximumWidth { Behavior on Layout.maximumWidth {
Anim { Anim {}
}
} }
Behavior on opacity { Behavior on opacity {
Anim { Anim {}
}
} }
Behavior on scale { Behavior on scale {
Anim { Anim {}
}
} }
} }
@@ -13,19 +13,16 @@ MaterialIcon {
animate: true animate: true
color: (Audio.sourceMuted ?? false) ? Colors.palette.m3error : Colors.palette.m3onSurface color: (Audio.sourceMuted ?? false) ? Colors.palette.m3error : Colors.palette.m3onSurface
fill: 1 fill: 1
font.pointSize: Tokens.font.size.larger fontStyle: Tokens.font.icon.small
text: Audio.sourceMuted ? "mic_off" : "mic" text: Audio.sourceMuted ? "mic_off" : "mic"
Behavior on Layout.maximumWidth { Behavior on Layout.maximumWidth {
Anim { Anim {}
}
} }
Behavior on opacity { Behavior on opacity {
Anim { Anim {}
}
} }
Behavior on scale { Behavior on scale {
Anim { Anim {}
}
} }
} }
@@ -15,20 +15,16 @@ Item {
implicitWidth: currentItem?.implicitWidth ?? 0 implicitWidth: currentItem?.implicitWidth ?? 0
Behavior on Layout.preferredHeight { Behavior on Layout.preferredHeight {
Anim { Anim {}
}
} }
Behavior on Layout.preferredWidth { Behavior on Layout.preferredWidth {
Anim { Anim {}
}
} }
Behavior on opacity { Behavior on opacity {
Anim { Anim {}
}
} }
Behavior on scale { Behavior on scale {
Anim { Anim {}
}
} }
Component.onCompleted: Battery.startDaemon() Component.onCompleted: Battery.startDaemon()
@@ -67,7 +63,7 @@ Item {
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
animate: true animate: true
fill: 1 fill: 1
font.pointSize: Tokens.font.size.larger fontStyle: Tokens.font.icon.small
text: { text: {
if (PowerProfiles.profile === PowerProfile.PowerSaver) if (PowerProfiles.profile === PowerProfile.PowerSaver)
return "energy_savings_leaf"; return "energy_savings_leaf";
+4 -4
View File
@@ -62,10 +62,10 @@ Item {
root.popouts.currentName = `traymenu${root.ind}`; root.popouts.currentName = `traymenu${root.ind}`;
root.popouts.currentCenter = Qt.binding(() => root.mapToItem(root.loader, root.implicitWidth / 2, 0).x); root.popouts.currentCenter = Qt.binding(() => root.mapToItem(root.loader, root.implicitWidth / 2, 0).x);
root.popouts.hasCurrent = true; root.popouts.hasCurrent = true;
if (visibilities.sidebar || visibilities.dashboard || visibilities.settings) { if (screenState.sidebar || screenState.dashboard || screenState.settings) {
visibilities.sidebar = false; screenState.sidebar = false;
visibilities.dashboard = false; screenState.dashboard = false;
visibilities.settings = false; screenState.settings = false;
} }
} }
} }
+1 -1
View File
@@ -42,7 +42,7 @@ WidgetBase {
Repeater { Repeater {
id: repeater id: repeater
model: SystemTray.items model: SystemTray.items.values.filter(i => i.status !== Status.Passive && i.hasMenu)
TrayItem { TrayItem {
id: trayItem id: trayItem
+1 -2
View File
@@ -27,14 +27,13 @@ WidgetBase {
MaterialIcon { MaterialIcon {
Layout.alignment: root.horizontal ? Qt.AlignVCenter : Qt.AlignHCenter Layout.alignment: root.horizontal ? Qt.AlignVCenter : Qt.AlignHCenter
font.pointSize: root.horizontal ? Tokens.font.size.larger : Tokens.font.size.large fontStyle: Tokens.font.icon.small
text: "package_2" text: "package_2"
} }
CustomText { CustomText {
Layout.alignment: root.horizontal ? Qt.AlignVCenter : Qt.AlignHCenter Layout.alignment: root.horizontal ? Qt.AlignVCenter : Qt.AlignHCenter
color: root.textColor color: root.textColor
font.pointSize: Tokens.font.size.normal
text: root.countUpdates text: root.countUpdates
} }
} }
+2 -4
View File
@@ -52,8 +52,7 @@ Item {
elide: Qt.ElideRight elide: Qt.ElideRight
elideWidth: root.maxSize - (root.horizontal ? root.current.anchors.leftMargin : 0) elideWidth: root.maxSize - (root.horizontal ? root.current.anchors.leftMargin : 0)
font.family: "Rubik" font: Tokens.font.body.small
font.pointSize: Tokens.font.size.normal
text: Hypr.activeToplevel?.title ?? qsTr("Desktop") text: Hypr.activeToplevel?.title ?? qsTr("Desktop")
onElideWidthChanged: root.current.text = elidedText onElideWidthChanged: root.current.text = elidedText
@@ -81,8 +80,7 @@ Item {
width: root.horizontal ? implicitWidth : implicitHeight width: root.horizontal ? implicitWidth : implicitHeight
Behavior on opacity { Behavior on opacity {
Anim { Anim {}
}
} }
transform: [ transform: [
Translate { Translate {
+3 -5
View File
@@ -17,7 +17,7 @@ Item {
required property bool horizontal required property bool horizontal
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen) readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen)
required property ShellScreen screen required property ShellScreen screen
readonly property real shortSize: Math.max(Config.bar.height, Tokens.bar.innerSize) readonly property real shortSize: Math.max(Config.bar.height, Tokens.sizes.bar.innerSize)
readonly property real size: (workspaceButtonWidth * workspacesShown) + activeWorkspaceMargin * 2 readonly property real size: (workspaceButtonWidth * workspacesShown) + activeWorkspaceMargin * 2
property int workspaceButtonWidth: (horizontal ? bgRect.implicitHeight : bgRect.implicitWidth) - root.activeWorkspaceMargin * 2 property int workspaceButtonWidth: (horizontal ? bgRect.implicitHeight : bgRect.implicitWidth) - root.activeWorkspaceMargin * 2
property int workspaceIndexInGroup: (effectiveActiveWorkspaceId - 1) % root.workspacesShown property int workspaceIndexInGroup: (effectiveActiveWorkspaceId - 1) % root.workspacesShown
@@ -30,14 +30,12 @@ Item {
Behavior on implicitHeight { Behavior on implicitHeight {
enabled: !root.horizontal enabled: !root.horizontal
Anim { Anim {}
}
} }
Behavior on implicitWidth { Behavior on implicitWidth {
enabled: root.horizontal enabled: root.horizontal
Anim { Anim {}
}
} }
CustomRect { CustomRect {
+2 -3
View File
@@ -181,8 +181,7 @@ Column {
} }
] ]
transitions: Transition { transitions: Transition {
AnchorAnim { AnchorAnim {}
}
} }
} }
@@ -246,7 +245,7 @@ Column {
anchors.centerIn: parent anchors.centerIn: parent
color: profiles.current === text ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface color: profiles.current === text ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
fill: profiles.current === text ? 1 : 0 fill: profiles.current === text ? 1 : 0
font.pointSize: Tokens.font.size.large * 2 fontStyle: Tokens.font.icon.extraLarge
grade: Colors.light ? 0 : (text === "balance" ? -25 : 0) grade: Colors.light ? 0 : (text === "balance" ? -25 : 0)
text: parent.icon text: parent.icon
+10 -8
View File
@@ -36,15 +36,17 @@ CustomClippingRect {
MaterialIcon { MaterialIcon {
id: noUpdatesIcon id: noUpdatesIcon
anchors.horizontalCenter: parent.horizontalCenter anchors.centerIn: parent
anchors.top: parent.top anchors.verticalCenterOffset: -noUpdatesText.implicitHeight
color: Colors.tPalette.m3onSurfaceVariant color: Colors.tPalette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.extraLarge * 3 fontStyle: Tokens.font.icon.builders.large.size(48).weight(Font.DemiBold).build()
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
text: "check" text: "check"
} }
CustomText { CustomText {
id: noUpdatesText
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.top: noUpdatesIcon.bottom anchors.top: noUpdatesIcon.bottom
color: Colors.tPalette.m3onSurfaceVariant color: Colors.tPalette.m3onSurfaceVariant
@@ -89,7 +91,7 @@ CustomClippingRect {
anchors.rightMargin: Tokens.padding.small anchors.rightMargin: Tokens.padding.small
MaterialIcon { MaterialIcon {
font.pointSize: Tokens.font.size.large * 2 fontStyle: Tokens.font.icon.extraLarge
text: "package_2" text: "package_2"
} }
@@ -100,7 +102,7 @@ CustomClippingRect {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 25 Layout.preferredHeight: 25
elide: Text.ElideRight elide: Text.ElideRight
font.pointSize: Tokens.font.size.large font: Tokens.font.title.medium
text: update.sections[0] text: update.sections[0]
} }
@@ -122,7 +124,7 @@ CustomClippingRect {
Layout.preferredWidth: 125 Layout.preferredWidth: 125
animate: true animate: true
color: Colors.palette.m3tertiary color: Colors.palette.m3tertiary
font.pointSize: Tokens.font.size.large font: Tokens.font.title.medium
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
marqueeEnabled: true marqueeEnabled: true
pauseMs: 4000 pauseMs: 4000
@@ -133,7 +135,7 @@ CustomClippingRect {
MaterialIcon { MaterialIcon {
Layout.fillHeight: true Layout.fillHeight: true
color: Colors.palette.m3secondary color: Colors.palette.m3secondary
font.pointSize: Tokens.font.size.extraLarge fontStyle: Tokens.font.icon.large
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
text: "arrow_right_alt" text: "arrow_right_alt"
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
@@ -146,7 +148,7 @@ CustomClippingRect {
Layout.preferredWidth: 120 Layout.preferredWidth: 120
animate: true animate: true
color: Colors.palette.m3primary color: Colors.palette.m3primary
font.pointSize: Tokens.font.size.large font: Tokens.font.title.medium
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
marqueeEnabled: true marqueeEnabled: true
pauseMs: 4000 pauseMs: 4000
+4 -6
View File
@@ -14,7 +14,7 @@ Item {
readonly property int itemHeight: Config.clipboard.sizes.itemHeight readonly property int itemHeight: Config.clipboard.sizes.itemHeight
required property ShellScreen screen required property ShellScreen screen
required property PersistentProperties visibilities required property ScreenState screenState
implicitHeight: search.implicitHeight + entries.anchors.topMargin + ((view.spacing + itemHeight) * Config.clipboard.maxEntriesShown) - view.spacing implicitHeight: search.implicitHeight + entries.anchors.topMargin + ((view.spacing + itemHeight) * Config.clipboard.maxEntriesShown) - view.spacing
implicitWidth: Config.clipboard.sizes.width + preview.width + preview.anchors.leftMargin implicitWidth: Config.clipboard.sizes.width + preview.width + preview.anchors.leftMargin
@@ -49,7 +49,7 @@ Item {
Keys.onUpPressed: view.decrementCurrentIndex() Keys.onUpPressed: view.decrementCurrentIndex()
onAccepted: { onAccepted: {
ClipHistory.copy(view.currentItem.modelData); ClipHistory.copy(view.currentItem.modelData);
root.visibilities.clipboard = false; root.screenState.clipboard = false;
} }
} }
} }
@@ -66,8 +66,7 @@ Item {
radius: 25 radius: 25
Behavior on implicitWidth { Behavior on implicitWidth {
Anim { Anim {}
}
} }
Column { Column {
@@ -104,7 +103,6 @@ Item {
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
color: Colors.palette.m3onSurfaceVariant color: Colors.palette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.small
text: lineRow.modelData.line text: lineRow.modelData.line
} }
} }
@@ -252,7 +250,7 @@ Item {
anchors.left: parent.left anchors.left: parent.left
anchors.margins: Tokens.padding.small anchors.margins: Tokens.padding.small
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
font.pointSize: Tokens.font.size.large fontStyle: Tokens.font.icon.large
text: clipItem.isImage ? "image" : "text_fields" text: clipItem.isImage ? "image" : "text_fields"
} }
+3 -3
View File
@@ -11,8 +11,8 @@ Item {
property int contentHeight property int contentHeight
property real offsetScale: shouldBeActive ? 0 : 1 property real offsetScale: shouldBeActive ? 0 : 1
required property ShellScreen screen required property ShellScreen screen
readonly property bool shouldBeActive: visibilities.clipboard && Config.clipboard.enabled readonly property bool shouldBeActive: screenState.clipboard && Config.clipboard.enabled
required property PersistentProperties visibilities required property ScreenState screenState
anchors.bottomMargin: (-implicitHeight - 5) * offsetScale anchors.bottomMargin: (-implicitHeight - 5) * offsetScale
implicitHeight: content.implicitHeight + Tokens.padding.small * 2 implicitHeight: content.implicitHeight + Tokens.padding.small * 2
@@ -35,7 +35,7 @@ Item {
sourceComponent: Content { sourceComponent: Content {
screen: root.screen screen: root.screen
visibilities: root.visibilities screenState: root.screenState
} }
} }
} }
+123 -36
View File
@@ -1,82 +1,169 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import Quickshell import Quickshell
import Quickshell.Widgets
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Config import ZShell.Config
import ZShell
import qs.Components import qs.Components
Item { Item {
id: root id: root
required property PersistentProperties dashState readonly property var dashboardTabs: {
readonly property real nonAnimHeight: view.implicitHeight + viewWrapper.anchors.margins * 2 const allTabs = [
{
component: dashComponent,
iconName: "dashboard",
text: qsTr("Dashboard"),
enabled: Config.dashboard.showDashboard
},
{
component: weatherComponent,
iconName: "cloud",
text: qsTr("Weather"),
enabled: Config.dashboard.showWeather
},
{
component: mediaComponent,
iconName: "queue_music",
text: qsTr("Media"),
enabled: Config.dashboard.showMedia
},
{
component: procsComponent,
iconName: "speed",
text: qsTr("Processes"),
enabled: Config.dashboard.showProcesses
},
];
return allTabs.filter(tab => tab.enabled);
}
readonly property real nonAnimHeight: tabs.implicitHeight + tabs.anchors.topMargin + view.implicitHeight + viewWrapper.anchors.margins + viewWrapper.anchors.topMargin
readonly property real nonAnimWidth: view.implicitWidth + viewWrapper.anchors.margins * 2 readonly property real nonAnimWidth: view.implicitWidth + viewWrapper.anchors.margins * 2
required property PersistentProperties visibilities required property ScreenState screenState
implicitHeight: nonAnimHeight implicitHeight: nonAnimHeight
implicitWidth: nonAnimWidth implicitWidth: nonAnimWidth
Behavior on implicitHeight { Behavior on implicitHeight {
Anim { Anim {
type: Anim.SlowEffects
} }
} }
Behavior on implicitWidth { Behavior on implicitWidth {
Anim { Anim {
type: Anim.SlowEffects
} }
} }
ClippingRectangle { Tabs {
id: tabs
anchors.left: parent.left
anchors.margins: Tokens.padding.small
anchors.right: parent.right
anchors.top: parent.top
anchors.topMargin: ZUtils.clamp(anchors.margins - Config.bar.border, 0, anchors.margins)
nonAnimWidth: view.implicitWidth
screenState: root.screenState
tabs: root.dashboardTabs
}
Item {
id: viewWrapper id: viewWrapper
property int radius: Tokens.rounding.extraLarge - anchors.margins
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.left: parent.left anchors.left: parent.left
anchors.margins: Tokens.padding.small anchors.margins: Tokens.padding.small
anchors.right: parent.right anchors.right: parent.right
anchors.top: parent.top anchors.top: tabs.bottom
color: "transparent" anchors.topMargin: Tokens.spacing.small
radius: Tokens.rounding.extraLarge - anchors.margins clip: view.currentIndex !== 3 || (!view.currentItem?.settled ?? true)
Item { Item {
id: view id: view
readonly property int currentIndex: root.dashState?.currentTab readonly property int currentIndex: root.screenState.dashboardTab
readonly property Item currentItem: row.children[currentIndex] readonly property Item currentItem: {
repeater.count;
return repeater.itemAt(currentIndex);
}
anchors.fill: parent anchors.fill: parent
implicitHeight: currentItem.implicitHeight implicitHeight: currentItem?.implicitHeight ?? 0
implicitWidth: currentItem.implicitWidth implicitWidth: currentItem?.implicitWidth ?? 0
RowLayout { Repeater {
id: row id: repeater
Pane { delegate: Loader {
index: 0 id: paneLoader
sourceComponent: Dash { required property int index
dashState: root.dashState required property var modelData
visibilities: root.visibilities
readonly property real targetX: index < view.currentIndex ? -view.implicitWidth : index > view.currentIndex ? view.implicitWidth : 0
readonly property bool settled: x === targetX
active: index === view.currentIndex || opacity > 0
opacity: index === view.currentIndex ? 1 : 0
sourceComponent: modelData.component
visible: opacity > 0
x: targetX
y: 0
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
} }
Behavior on x {
Anim {
type: Anim.DefaultEffects
}
}
}
model: ScriptModel {
values: root.dashboardTabs
}
}
Component {
id: dashComponent
Dash {
radius: viewWrapper.radius
screenState: root.screenState
}
}
Component {
id: weatherComponent
WeatherTab {
radius: viewWrapper.radius
}
}
Component {
id: mediaComponent
Media {
radius: viewWrapper.radius
screenState: root.screenState
}
}
Component {
id: procsComponent
ProcessesTab {
radius: viewWrapper.radius
} }
} }
} }
} }
component Pane: Loader {
id: pane
required property int index
Layout.alignment: Qt.AlignTop
Component.onCompleted: active = Qt.binding(() => {
// Always keep current tab loaded
if (pane.index === view.currentIndex)
return true;
const vx = Math.floor(view.visibleArea.xPosition * view.contentWidth);
const vex = Math.floor(vx + view.visibleArea.widthRatio * view.contentWidth);
return (vx >= x && vx <= x + implicitWidth) || (vex >= x && vex <= x + implicitWidth);
})
}
} }
+7 -23
View File
@@ -11,10 +11,9 @@ import qs.Services
GridLayout { GridLayout {
id: root id: root
required property PersistentProperties dashState readonly property bool dashboardVisible: screenState.dashboard
readonly property bool dashboardVisible: visibilities.dashboard required property int radius
property int radius: Tokens.rounding.medium required property ScreenState screenState
required property PersistentProperties visibilities
columnSpacing: Tokens.spacing.small columnSpacing: Tokens.spacing.small
rowSpacing: Tokens.spacing.small rowSpacing: Tokens.spacing.small
@@ -61,19 +60,18 @@ GridLayout {
User { User {
id: user id: user
dashState: root.dashState screenState: root.screenState
} }
} }
Rect { Rect {
Layout.columnSpan: 2 Layout.columnSpan: 2
Layout.fillHeight: true Layout.fillHeight: true
Layout.preferredWidth: Config.dashboard.sizes.weatherWidth Layout.preferredWidth: Tokens.sizes.dashboard.weatherWidth
Layout.row: 0 Layout.row: 0
radius: root.radius radius: root.radius
Weather { Weather {}
}
} }
Rect { Rect {
@@ -87,7 +85,7 @@ GridLayout {
Calendar { Calendar {
id: calendar id: calendar
dashState: root.dashState screenState: root.screenState
} }
} }
@@ -104,20 +102,6 @@ GridLayout {
} }
} }
CustomClippingRect {
Layout.column: 0
Layout.columnSpan: 5
Layout.fillWidth: true
Layout.preferredHeight: media.implicitHeight
Layout.row: 2
color: Colors.tPalette.m3surfaceContainer
radius: root.radius
Media {
id: media
}
}
component Rect: CustomRect { component Rect: CustomRect {
color: Colors.tPalette.m3surfaceContainer color: Colors.tPalette.m3surfaceContainer
} }
+9 -9
View File
@@ -12,12 +12,12 @@ CustomMouseArea {
id: root id: root
property int activeGrid: 1 property int activeGrid: 1
required property PersistentProperties dashState required property ScreenState screenState
property bool initialized: false property bool initialized: false
property int month1 property int month1
property int month2 property int month2
readonly property int realCurrMonth: dashState.currentDate.getMonth() readonly property int realCurrMonth: screenState.dashboardDate.getMonth()
readonly property int realCurrYear: dashState.currentDate.getFullYear() readonly property int realCurrYear: screenState.dashboardDate.getFullYear()
property int year1 property int year1
property int year2 property int year2
@@ -43,9 +43,9 @@ CustomMouseArea {
function onWheel(event: WheelEvent): void { function onWheel(event: WheelEvent): void {
if (event.angleDelta.y > 0) if (event.angleDelta.y > 0)
root.dashState.currentDate = new Date(root.realCurrYear, root.realCurrMonth - 1, 1); root.screenState.dashboardDate = new Date(root.realCurrYear, root.realCurrMonth - 1, 1);
else if (event.angleDelta.y < 0) else if (event.angleDelta.y < 0)
root.dashState.currentDate = new Date(root.realCurrYear, root.realCurrMonth + 1, 1); root.screenState.dashboardDate = new Date(root.realCurrYear, root.realCurrMonth + 1, 1);
} }
acceptedButtons: Qt.MiddleButton acceptedButtons: Qt.MiddleButton
@@ -61,7 +61,7 @@ CustomMouseArea {
initialized = true; initialized = true;
} }
onClicked: root.dashState.currentDate = new Date() onClicked: root.screenState.dashboardDate = new Date()
onRealCurrMonthChanged: handleDateChange() onRealCurrMonthChanged: handleDateChange()
onRealCurrYearChanged: handleDateChange() onRealCurrYearChanged: handleDateChange()
@@ -135,7 +135,7 @@ CustomMouseArea {
padding: Tokens.padding.extraSmall padding: Tokens.padding.extraSmall
type: IconButton.Text type: IconButton.Text
onClicked: root.dashState.currentDate = new Date(root.realCurrYear, root.realCurrMonth - 1, 1) onClicked: root.screenState.dashboardDate = new Date(root.realCurrYear, root.realCurrMonth - 1, 1)
} }
Item { Item {
@@ -158,7 +158,7 @@ CustomMouseArea {
} }
} }
onClicked: root.dashState.currentDate = new Date() onClicked: root.screenState.dashboardDate = new Date()
} }
Item { Item {
@@ -206,7 +206,7 @@ CustomMouseArea {
padding: Tokens.padding.extraSmall padding: Tokens.padding.extraSmall
type: IconButton.Text type: IconButton.Text
onClicked: root.dashState.currentDate = new Date(root.realCurrYear, root.realCurrMonth + 1, 1) onClicked: root.screenState.dashboardDate = new Date(root.realCurrYear, root.realCurrMonth + 1, 1)
} }
} }
-51
View File
@@ -1,51 +0,0 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import qs.Components
import ZShell.Config
import qs.Helpers
import qs.Services
Item {
id: root
anchors.bottom: parent.bottom
anchors.top: parent.top
implicitWidth: 110
ColumnLayout {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 0
CustomText {
Layout.alignment: Qt.AlignHCenter
Layout.bottomMargin: -(font.pointSize * 0.4)
color: Colors.palette.m3secondary
font.family: "Rubik"
font.pointSize: 18
font.weight: 600
text: Time.hourStr
}
CustomText {
Layout.alignment: Qt.AlignHCenter
color: Colors.palette.m3primary
font.family: "Rubik"
font.pointSize: 18 * 0.9
text: "•••"
}
CustomText {
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: -(font.pointSize * 0.4)
color: Colors.palette.m3secondary
font.family: "Rubik"
font.pointSize: 18
font.weight: 600
text: Time.minuteStr
}
}
}
-329
View File
@@ -1,329 +0,0 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Services.Mpris
import QtQuick
import QtQuick.Layouts
import QtQuick.Shapes
import QtQuick.Effects
import ZShell.Components
import ZShell.Services
import ZShell.Config
import qs.Modules.Dashboard.Dash.Media
import qs.Daemons
import qs.Components
import qs.Helpers
import qs.Services
Item {
id: root
readonly property bool hasUnknownLength: (Players.active?.length ?? 0) > 2147483647
property real playerProgress: {
const active = Players.active;
return active?.length ? (active.position % active.length) / active.length : 0;
}
property int rowHeight: Tokens.padding.large + Config.dashboard.sizes.mediaProgressThickness + Tokens.spacing.small
anchors.left: parent.left
anchors.right: parent.right
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
function lengthStr(length: int): string {
if (length < 0)
return "-1:-1";
const hours = Math.floor(length / 3600);
const mins = Math.floor((length % 3600) / 60);
const secs = Math.floor(length % 60).toString().padStart(2, "0");
if (hours > 0)
return `${hours}:${mins.toString().padStart(2, "0")}:${secs}`;
return `${mins}:${secs}`;
}
Behavior on playerProgress {
Anim {
duration: Tokens.anim.durations.large
}
}
Timer {
running: Players.active?.isPlaying ?? false
interval: Config.dashboard.mediaUpdateInterval
triggeredOnStart: true
repeat: true
onTriggered: Players.active?.positionChanged()
}
ServiceRef {
service: Audio.cava
}
BackgroundShapes {
anchors.fill: parent
}
Shape {
id: visualizer
readonly property int bars: Config.services.visualizerBars
property color color: Qt.alpha(Colors.palette.m3primary, 0.5)
property color fillColor: Qt.alpha(color, 0.1)
anchors.fill: parent
anchors.leftMargin: -(shape.strokeWidth / 2)
layer.enabled: true
asynchronous: true
preferredRendererType: Shape.CurveRenderer
ShapePath {
id: shape
strokeColor: visualizer.color
fillColor: visualizer.fillColor
strokeWidth: 2
PathSvg {
path: shape.curvePath
}
property string curvePath: {
const values = Audio.cava.values;
const n = values.length;
if (n < 2)
return "";
const h = root.height + shape.strokeWidth / 2;
const w = root.width + shape.strokeWidth;
function x(i) {
return i * w / (n - 1);
}
function y(i) {
return h - (values[i] * Config.dashboard.sizes.mediaVisualizerSize) / 2;
}
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(Colors.palette.m3shadow, 0.2)
layer.enabled: true
layer.effect: MultiEffect {
maskEnabled: true
maskSource: visualizer
}
}
RowLayout {
id: layout
anchors.fill: parent
anchors.margins: Tokens.padding.large
anchors.leftMargin: Tokens.padding.extraLarge
CoverVisualizer {
Layout.fillHeight: true
implicitWidth: Config.dashboard.sizes.mediaCoverArtSize
}
ColumnLayout {
Layout.fillWidth: true
Layout.leftMargin: Tokens.spacing.largeIncreased
Layout.rightMargin: Tokens.spacing.largeIncreased
spacing: Tokens.spacing.small
MarqueeText {
id: title
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
color: Colors.palette.m3primary
font.pointSize: Tokens.font.size.normal
horizontalAlignment: Text.AlignHCenter
pauseMs: 4000
text: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title")
implicitWidth: parent.width - Tokens.padding.large * 4
}
MarqueeText {
id: album
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
color: Colors.palette.m3outline
font.pointSize: Tokens.font.size.small
horizontalAlignment: Text.AlignHCenter
pauseMs: 4000
text: (Players.active?.trackAlbum ?? qsTr("No media")) || qsTr("Unknown album")
implicitWidth: parent.width - Tokens.padding.large * 4
}
MarqueeText {
id: artist
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
color: Colors.palette.m3secondary
horizontalAlignment: Text.AlignHCenter
pauseMs: 4000
text: (Players.active?.trackArtist ?? qsTr("No media")) || qsTr("Unknown artist")
implicitWidth: parent.width - Tokens.padding.large * 4
}
RowLayout {
id: positionSliderLayout
Layout.topMargin: Tokens.spacing.large
Layout.fillWidth: true
spacing: Tokens.spacing.small
TextMetrics {
id: timeMetrics
text: Players.active ? root.lengthStr(Math.max(Players.active.position, root.hasUnknownLength ? 0 : Players.active.length)).replace(/[1-9]/g, "0") : "00:00"
}
CustomText {
id: positionLabel
Layout.preferredWidth: timeMetrics.width
text: root.lengthStr(Players.active?.position ?? -1)
color: Colors.palette.m3onSurfaceVariant
font: timeMetrics.font
horizontalAlignment: Text.AlignHCenter
}
CustomSlider {
id: positionSlider
Layout.fillWidth: true
value: Players.active ? Players.active.position / (Players.active.length || 1) : 0
enabled: (Players.active?.canSeek ?? false) && !root.hasUnknownLength
wavy: true
animateWave: Players.active?.isPlaying ?? false
waveFrequency: 5
waveDuration: 2000
interactionOnMove: false
onInteraction: value => {
const active = Players.active;
if (active?.canSeek && active?.positionSupported)
active.position = value * active.length;
}
Binding {
target: positionLabel
property: "text"
value: root.lengthStr(positionSlider.pos * (Players.active?.length ?? 0))
when: positionSlider.dragging
}
}
CustomText {
Layout.preferredWidth: timeMetrics.width
text: root.hasUnknownLength ? "--:--" : root.lengthStr(Players.active?.length ?? -1)
color: Colors.palette.m3onSurfaceVariant
font: timeMetrics.font
horizontalAlignment: Text.AlignHCenter
}
}
ButtonRow {
Layout.topMargin: Tokens.spacing.largeIncreased
Layout.fillWidth: true
spacing: Tokens.spacing.extraSmall
Layout.leftMargin: positionSliderLayout.implicitWidth / 4
Layout.rightMargin: positionSliderLayout.implicitWidth / 4
IconButton {
type: IconButton.Tonal
icon: "shuffle"
isRound: true
shapeMorph: true
checked: Players.active?.shuffle ?? false
enabled: Players.active?.shuffleSupported
onClicked: Players.active.shuffle = !Players.active?.shuffle
}
IconButton {
id: previousBtn
type: IconButton.Tonal
icon: "skip_previous"
isRound: true
shapeMorph: true
enabled: Players.active?.canGoPrevious
onClicked: Players.active?.previous()
font.pointSize: Tokens.font.size.large
}
IconButton {
id: playPauseBtn
icon: Players.active?.isPlaying ? "pause" : "play_arrow"
isRound: true
shapeMorph: true
fillWidth: true
checked: Players.active?.isPlaying ?? false
enabled: Players.active?.canTogglePlaying
onClicked: Players.active?.togglePlaying()
font.pointSize: Tokens.font.size.large
}
IconButton {
id: nextBtn
type: IconButton.Tonal
icon: "skip_next"
isRound: true
shapeMorph: true
enabled: Players.active?.canGoNext
onClicked: Players.active?.next()
font.pointSize: Tokens.font.size.large
}
IconButton {
type: IconButton.Tonal
icon: Players.active?.loopState === MprisLoopState.Track ? "repeat_one" : "repeat"
isRound: true
shapeMorph: true
checked: Players.active?.loopState === MprisLoopState.Track || Players.active?.loopState === MprisLoopState.Playlist
enabled: Players.active?.loopSupported
onClicked: {
const state = Players.active.loopState;
if (state === MprisLoopState.None)
Players.active.loopState = MprisLoopState.Track;
else if (state === MprisLoopState.Track)
Players.active.loopState = MprisLoopState.Playlist;
else
Players.active.loopState = MprisLoopState.None;
}
}
}
}
}
}
+7 -10
View File
@@ -9,7 +9,7 @@ import qs.Helpers
Row { Row {
id: root id: root
required property PersistentProperties dashState required property ScreenState screenState
padding: 20 padding: 20
spacing: 12 spacing: 12
@@ -23,8 +23,7 @@ Row {
MaterialIcon { MaterialIcon {
anchors.centerIn: parent anchors.centerIn: parent
fill: 1 fill: 1
font.pointSize: Math.floor(info.implicitHeight / 2) || 1 fontStyle: Tokens.font.icon.large
grade: 200
text: "person" text: "person"
} }
@@ -52,7 +51,7 @@ Row {
id: icon id: icon
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: (Config.dashboard.sizes.infoIconSize - implicitWidth) / 2 anchors.leftMargin: (Tokens.sizes.dashboard.infoIconSize - implicitWidth) / 2
color: Colors.palette.m3primary color: Colors.palette.m3primary
implicitSize: Math.floor(13 * 1.34) implicitSize: Math.floor(13 * 1.34)
source: SystemInfo.osLogo source: SystemInfo.osLogo
@@ -65,9 +64,8 @@ Row {
anchors.leftMargin: icon.anchors.leftMargin anchors.leftMargin: icon.anchors.leftMargin
anchors.verticalCenter: icon.verticalCenter anchors.verticalCenter: icon.verticalCenter
elide: Text.ElideRight elide: Text.ElideRight
font.pointSize: 13
text: `: ${SystemInfo.osPrettyName || SystemInfo.osName}` text: `: ${SystemInfo.osPrettyName || SystemInfo.osName}`
width: Config.dashboard.sizes.infoWidth width: Tokens.sizes.dashboard.infoWidth
} }
} }
@@ -100,10 +98,10 @@ Row {
id: icon id: icon
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: (Config.dashboard.sizes.infoIconSize - implicitWidth) / 2 anchors.leftMargin: (Tokens.sizes.dashboard.infoIconSize - implicitWidth) / 2
color: line.colour color: line.colour
fill: 1 fill: 1
font.pointSize: 13 fontStyle: Tokens.font.icon.small
text: line.icon text: line.icon
} }
@@ -114,9 +112,8 @@ Row {
anchors.leftMargin: icon.anchors.leftMargin anchors.leftMargin: icon.anchors.leftMargin
anchors.verticalCenter: icon.verticalCenter anchors.verticalCenter: icon.verticalCenter
elide: Text.ElideNone elide: Text.ElideNone
font.pointSize: 13
text: `: ${line.text}` text: `: ${line.text}`
width: Config.dashboard.sizes.infoWidth width: Tokens.sizes.dashboard.infoWidth
} }
} }
} }
+2 -3
View File
@@ -19,7 +19,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
animate: true animate: true
color: Colors.palette.m3secondary color: Colors.palette.m3secondary
font.pointSize: 54 fontStyle: Tokens.font.icon.extraLarge
text: Weather.icon text: Weather.icon
} }
@@ -35,8 +35,7 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
animate: true animate: true
color: Colors.palette.m3primary color: Colors.palette.m3primary
font.pointSize: Tokens.font.size.extraLarge font: Tokens.font.body.large
font.weight: 500
text: Weather.temp text: Weather.temp
} }
+158
View File
@@ -0,0 +1,158 @@
import QtQuick
import QtQuick.Layouts
import M3Shapes
import ZShell.Config
import qs.Modules.Dashboard.Media
import qs.Components
import qs.Services
import qs.Helpers
Item {
id: root
property ScreenState screenState
required property int radius
implicitWidth: 1000
implicitHeight: 320
CustomClippingRect {
anchors.fill: parent
radius: root.radius
BackgroundShapes {
anchors.fill: parent
}
}
RowLayout {
anchors.fill: parent
anchors.margins: Tokens.padding.large
spacing: Tokens.spacing.extraLarge
CoverVisualizer {
Layout.fillHeight: true
implicitWidth: 250
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
state: Players.active ? "" : "noMedia"
states: State {
name: "noMedia"
PropertyChanges {
noMedia.opacity: 1
content.opacity: 0
}
}
transitions: [
Transition {
from: ""
SequentialAnimation {
Anim {
target: content
property: "opacity"
type: Anim.DefaultEffects
}
Anim {
target: noMedia
property: "opacity"
type: Anim.SlowEffects
}
}
},
Transition {
to: ""
SequentialAnimation {
Anim {
target: noMedia
property: "opacity"
type: Anim.DefaultEffects
}
Anim {
target: content
property: "opacity"
type: Anim.SlowEffects
}
}
}
]
Loader {
id: noMedia
anchors.centerIn: parent
anchors.horizontalCenterOffset: -Tokens.padding.extraLarge * 2
asynchronous: true
active: opacity > 0
opacity: 0
sourceComponent: ColumnLayout {
spacing: Tokens.spacing.small
MaterialShape {
Layout.topMargin: (pathBounds().height - implicitSize) / 2
Layout.bottomMargin: (pathBounds().height - implicitSize) / 2 + Tokens.spacing.small
Layout.alignment: Qt.AlignHCenter
color: Colors.palette.m3primaryContainer
implicitSize: icon.implicitHeight + Tokens.padding.extraLarge * 2
shape: MaterialShape.ClamShell
Behavior on color {
CAnim {}
}
MaterialIcon {
id: icon
anchors.centerIn: parent
text: "queue_music"
fontStyle: Tokens.font.icon.large
color: Colors.palette.m3onPrimaryContainer
}
}
CustomText {
Layout.alignment: Qt.AlignHCenter
text: qsTr("Nothing playing")
font: Tokens.font.body.medium
}
CustomText {
text: qsTr("Play something for it to show up here!")
color: Colors.palette.m3onSurfaceVariant
font: Tokens.font.body.large
}
}
}
Loader {
id: content
anchors.fill: parent
asynchronous: true
active: opacity > 0
sourceComponent: RowLayout {
spacing: Tokens.spacing.extraLarge
Details {
Layout.fillWidth: true
}
LyricsAndSelector {
Layout.fillHeight: true
implicitWidth: 300
}
}
}
}
}
}
@@ -11,8 +11,8 @@ Item {
id: cover id: cover
anchors.centerIn: parent anchors.centerIn: parent
implicitHeight: Config.dashboard.sizes.mediaCoverArtSize implicitHeight: 200
implicitWidth: Config.dashboard.sizes.mediaCoverArtSize implicitWidth: 200
shape.shape: MaterialShape.Cookie9Sided shape.shape: MaterialShape.Cookie9Sided
} }
} }
+194
View File
@@ -0,0 +1,194 @@
import QtQuick
import QtQuick.Layouts
import Quickshell.Services.Mpris
import ZShell.Components
import ZShell.Config
import qs.Components
import qs.Services
import qs.Helpers
ColumnLayout {
id: root
readonly property bool hasUnknownLength: (Players.active?.length ?? 0) > 2147483647
function lengthStr(length: int): string {
if (length < 0)
return "-1:-1";
const hours = Math.floor(length / 3600);
const mins = Math.floor((length % 3600) / 60);
const secs = Math.floor(length % 60).toString().padStart(2, "0");
if (hours > 0)
return `${hours}:${mins.toString().padStart(2, "0")}:${secs}`;
return `${mins}:${secs}`;
}
spacing: Tokens.spacing.extraSmall
Timer {
running: Players.active?.isPlaying ?? false
interval: Config.dashboard.mediaUpdateInterval
triggeredOnStart: true
repeat: true
onTriggered: Players.active?.positionChanged()
}
CustomText {
Layout.fillWidth: true
text: Players.active?.trackTitle ?? ""
font: Tokens.font.title.large
elide: Text.ElideRight
animate: true
}
CustomText {
Layout.fillWidth: true
text: Players.active?.trackArtist || qsTr("Unknown artist")
color: Colors.palette.m3onSurfaceVariant
font: Tokens.font.title.medium
elide: Text.ElideRight
animate: true
}
CustomText {
Layout.fillWidth: true
text: Players.active?.trackAlbum || qsTr("Unknown album")
color: Colors.palette.m3secondary
font: Tokens.font.title.medium
elide: Text.ElideRight
animate: true
}
RowLayout {
Layout.topMargin: Tokens.spacing.extraLargeIncreased
Layout.fillWidth: true
spacing: Tokens.spacing.small
TextMetrics {
id: timeMetrics
text: Players.active ? root.lengthStr(Math.max(Players.active.position, root.hasUnknownLength ? 0 : Players.active.length)).replace(/[1-9]/g, "0") : "00:00"
font: Tokens.font.label.medium
}
CustomText {
id: positionLabel
Layout.preferredWidth: timeMetrics.width
text: root.lengthStr(Players.active?.position ?? -1)
color: Colors.palette.m3onSurfaceVariant
font: timeMetrics.font
horizontalAlignment: Text.AlignHCenter
}
CustomSlider {
id: positionSlider
Layout.fillWidth: true
value: Players.active ? Players.active.position / (Players.active.length || 1) : 0
enabled: (Players.active?.canSeek ?? false) && !root.hasUnknownLength
wavy: true
animateWave: Players.active?.isPlaying ?? false
waveFrequency: 5
waveDuration: 2000
interactionOnMove: false
onInteraction: value => {
const active = Players.active;
if (active?.canSeek && active?.positionSupported)
active.position = value * active.length;
}
Binding {
target: positionLabel
property: "text"
value: root.lengthStr(positionSlider.pos * (Players.active?.length ?? 0))
when: positionSlider.dragging
}
}
CustomText {
Layout.preferredWidth: timeMetrics.width
text: root.hasUnknownLength ? "--:--" : root.lengthStr(Players.active?.length ?? -1)
color: Colors.palette.m3onSurfaceVariant
font: timeMetrics.font
horizontalAlignment: Text.AlignHCenter
}
}
ButtonRow {
Layout.topMargin: Tokens.spacing.largeIncreased
Layout.fillWidth: true
spacing: Tokens.spacing.extraSmall
IconButton {
type: IconButton.Tonal
icon: "shuffle"
isRound: true
shapeMorph: true
checked: Players.active?.shuffle ?? false
font: Tokens.font.icon.builders.medium.weight(Font.Medium).build()
enabled: Players.active?.shuffleSupported
onClicked: Players.active.shuffle = !Players.active?.shuffle
implicitWidth: Math.round(implicitHeight * 0.9)
}
IconButton {
id: previousBtn
type: IconButton.Tonal
icon: "skip_previous"
isRound: true
shapeMorph: true
font: Tokens.font.icon.large
enabled: Players.active?.canGoPrevious
onClicked: Players.active?.previous()
}
IconButton {
id: playPauseBtn
icon: Players.active?.isPlaying ? "pause" : "play_arrow"
isRound: true
shapeMorph: true
fillWidth: true
checked: Players.active?.isPlaying ?? false
font: Tokens.font.icon.large
enabled: Players.active?.canTogglePlaying
onClicked: Players.active?.togglePlaying()
}
IconButton {
id: nextBtn
type: IconButton.Tonal
icon: "skip_next"
isRound: true
shapeMorph: true
font: Tokens.font.icon.large
enabled: Players.active?.canGoNext
onClicked: Players.active?.next()
}
IconButton {
type: IconButton.Tonal
icon: Players.active?.loopState === MprisLoopState.Track ? "repeat_one" : "repeat"
isRound: true
shapeMorph: true
checked: Players.active?.loopState === MprisLoopState.Track || Players.active?.loopState === MprisLoopState.Playlist
font: Tokens.font.icon.builders.medium.weight(Font.Medium).build()
enabled: Players.active?.loopSupported
onClicked: {
const state = Players.active.loopState;
if (state === MprisLoopState.None)
Players.active.loopState = MprisLoopState.Track;
else if (state === MprisLoopState.Track)
Players.active.loopState = MprisLoopState.Playlist;
else
Players.active.loopState = MprisLoopState.None;
}
implicitWidth: Math.round(implicitHeight * 0.9)
}
}
}
+317
View File
@@ -0,0 +1,317 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Effects
import QtQuick.Layouts
import ZShell.Config
import ZShell.Services
import qs.Components
import qs.Effects
import qs.Services
import qs.Helpers
Item {
id: root
// Funny binding hack to make lyrics update
readonly property var _: {
const p = Players.active;
if (p)
Lyrics.setTrack(p.trackArtist, p.trackTitle, p.trackAlbum, p.length);
else
Lyrics.clearTrack();
}
readonly property real fadeAmount: 0.1
property list<string> lyricList: Lyrics.lyrics
layer.enabled: true
layer.effect: Mask {
maskSource: mask
Rectangle {
id: mask
layer.enabled: true
visible: false
implicitWidth: root.width
implicitHeight: root.height
gradient: Gradient {
orientation: Gradient.Vertical
GradientStop {
color: Qt.alpha("black", 0)
position: 0
}
GradientStop {
color: Qt.alpha("black", 1)
position: root.fadeAmount
}
GradientStop {
color: Qt.alpha("black", 1)
position: 1 - root.fadeAmount
}
GradientStop {
color: Qt.alpha("black", 0)
position: 1
}
}
}
}
state: {
if (Lyrics.hasLyrics)
return "hasLyrics";
if (Lyrics.loading)
return "loading";
return "noLyrics";
}
states: [
State {
name: "loading"
PropertyChanges {
loadingIndicator.opacity: 1
lyrics.opacity: 0
noLyrics.opacity: 0
}
},
State {
name: "hasLyrics"
PropertyChanges {
loadingIndicator.opacity: 0
lyrics.opacity: 1
noLyrics.opacity: 0
}
},
State {
name: "noLyrics"
PropertyChanges {
loadingIndicator.opacity: 0
lyrics.opacity: 0
noLyrics.opacity: 1
}
}
]
transitions: [
Transition {
from: "loading"
SequentialAnimation {
Anim {
target: loadingIndicator
property: "opacity"
type: Anim.DefaultEffects
}
Anim {
targets: [lyrics, noLyrics]
property: "opacity"
type: Anim.SlowEffects
}
}
},
Transition {
from: "hasLyrics"
SequentialAnimation {
Anim {
target: lyrics
property: "opacity"
type: Anim.DefaultEffects
}
Anim {
targets: [loadingIndicator, noLyrics]
property: "opacity"
type: Anim.SlowEffects
}
}
},
Transition {
from: "noLyrics"
SequentialAnimation {
Anim {
target: noLyrics
property: "opacity"
type: Anim.DefaultEffects
}
Anim {
targets: [loadingIndicator, lyrics]
property: "opacity"
type: Anim.SlowEffects
}
}
}
]
Loader {
id: loadingIndicator
anchors.centerIn: parent
asynchronous: true
active: opacity > 0
opacity: 0
sourceComponent: ColumnLayout {
spacing: Tokens.spacing.large
CustomRect {
Layout.alignment: Qt.AlignHCenter
implicitWidth: shape.implicitSize + Tokens.padding.medium * 2
implicitHeight: shape.implicitSize + Tokens.padding.medium * 2
color: Colors.palette.m3primaryContainer
radius: Tokens.rounding.full
LoadingIndicator {
id: shape
anchors.centerIn: parent
implicitSize: Math.round(300 / 5)
containsIcon: true // This removes the pentagon, which is not centered
}
}
CustomText {
text: qsTr("Loading lyrics...")
color: Colors.palette.m3onSurfaceVariant
font: Tokens.font.body.medium
}
}
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
}
Loader {
id: noLyrics
anchors.centerIn: parent
asynchronous: true
active: opacity > 0
opacity: 0
sourceComponent: ColumnLayout {
spacing: Tokens.spacing.small
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
text: "sentiment_sad"
fontStyle: Tokens.font.icon.extraLarge
color: Colors.palette.m3outline
}
CustomText {
text: qsTr("No lyrics found")
color: Colors.palette.m3outline
font: Tokens.font.body.medium
}
}
}
CustomListView {
id: lyrics
anchors.fill: parent
anchors.topMargin: parent.height * root.fadeAmount / 2
anchors.bottomMargin: parent.height * root.fadeAmount / 2
displayMarginBeginning: anchors.topMargin
displayMarginEnd: anchors.bottomMargin
model: root.lyricList
Component.onCompleted: {
currentIndex = Qt.binding(() => {
model; // Force update when lyrics change
return Lyrics.indexForTime(Players.active?.position ?? 0);
});
positionViewAtIndex(currentIndex, ListView.Center);
}
onModelChanged: Qt.callLater(() => positionViewAtIndex(currentIndex, ListView.Center))
highlightRangeMode: ListView.ApplyRange
highlightMoveDuration: Tokens.anim.durations.large
highlightMoveVelocity: -1
preferredHighlightBegin: (height - (currentItem?.implicitHeight ?? 0)) / 2
preferredHighlightEnd: (height + (currentItem?.implicitHeight ?? 0)) / 2
spacing: Tokens.spacing.small
opacity: 0
enabled: opacity > 0
delegate: CustomText {
id: lyric
required property string modelData
required property int index
property real effectScale: ListView.isCurrentItem ? 1 : 0
anchors.left: lyrics.contentItem.left
anchors.right: lyrics.contentItem.right
text: modelData || ". . ."
color: ListView.isCurrentItem ? Colors.palette.m3primary : mouse.containsMouse ? Colors.palette.m3onSurface : Colors.palette.m3outline
font: Tokens.font.body.medium
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
layer.enabled: effectScale > 0
layer.effect: MultiEffect {
shadowEnabled: true
shadowColor: Colors.palette.m3primary
shadowOpacity: 0.5 * lyric.effectScale
shadowBlur: 0.6 * lyric.effectScale
blur: 0.4 * lyric.effectScale
}
Behavior on effectScale {
Anim {
type: Anim.SlowEffects
}
}
MouseArea {
id: mouse
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onClicked: {
const p = Players.active;
if (p)
p.position = Lyrics.timeForIndex(lyric.index);
}
}
}
Behavior on opacity {
Anim {
type: Anim.SlowEffects
}
}
}
Behavior on lyricList {
SequentialAnimation {
Anim {
target: lyrics
property: "opacity"
to: 0
type: Anim.DefaultEffects
}
PropertyAction {}
Anim {
target: lyrics
property: "opacity"
to: 1
type: Anim.SlowEffects
}
}
}
}
@@ -0,0 +1,79 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.Mpris
import ZShell.Config
import qs.Components
import qs.Helpers
import qs.Services
Item {
ColumnLayout {
id: layout
anchors.fill: parent
anchors.leftMargin: Tokens.padding.medium
spacing: Tokens.spacing.medium
RowLayout {
Layout.bottomMargin: -Tokens.spacing.medium
spacing: Tokens.spacing.medium
z: 1
MaterialIcon {
Layout.topMargin: Math.round(fontInfo.pointSize * 0.12)
text: "lyrics"
fontStyle: Tokens.font.icon.large
}
CustomText {
Layout.fillWidth: true
text: qsTr("Lyrics")
font: Tokens.font.body.large
}
LyricsInfo {}
}
LyricList {
Layout.fillWidth: true
Layout.fillHeight: true
}
SplitButton {
Layout.alignment: Qt.AlignHCenter
type: SplitButton.Tonal
enabled: Players.list.length
active: menuItems.find(m => m.modelData === Players.active) ?? menuItems[0] ?? null
menu.onItemSelected: item => Players.manualActive = (item as PlayerItem).modelData
menuItems: playerList.instances
fallbackIcon: "music_off"
fallbackText: qsTr("No players", "no media players active")
minLeftWidth: layout.width - expandBtn.implicitWidth - spacing
label.Layout.maximumWidth: minLeftWidth - iconLabel.implicitWidth - textRow.spacing - textRow.anchors.horizontalCenterOffset / 2 - horizontalPadding * 2
label.elide: Text.ElideRight
stateLayer.enabled: true
menuOnTop: true
Variants {
id: playerList
model: Players.list
PlayerItem {}
}
}
}
component PlayerItem: MenuItem {
required property MprisPlayer modelData
icon: modelData === Players.active ? "check" : ""
text: Players.getIdentity(modelData)
activeIcon: "animated_images"
}
}
+212
View File
@@ -0,0 +1,212 @@
import QtQuick
import QtQuick.Layouts
import ZShell
import ZShell.Blobs
import ZShell.Config
import ZShell.Services
import qs.Components
import qs.Services
Item {
id: root
property bool open
readonly property real padding: Tokens.padding.large
implicitWidth: btn.implicitWidth * 0.9
implicitHeight: btn.implicitHeight * 0.9
BlobGroup {
id: blobGroup
color: Colors.palette.m3surfaceContainerHighest
smoothing: Tokens.rounding.medium
cornerFill: false
Behavior on color {
CAnim {}
}
}
BlobRect {
id: btnRect
anchors.fill: parent
anchors.margins: !btn.pressed && btn.containsMouse ? -Tokens.padding.extraSmall : 0
group: blobGroup
radius: Tokens.rounding.medium
Behavior on anchors.margins {
Anim {}
}
}
BlobRect {
id: rect
anchors.right: parent.right
anchors.top: parent.top
implicitWidth: parent.width
implicitHeight: parent.height
group: blobGroup
radius: Tokens.rounding.medium
deformScale: 0.00001
states: State {
name: "open"
when: root.open
PropertyChanges {
rect.anchors.rightMargin: root.width - Tokens.spacing.small
rect.anchors.topMargin: -Tokens.padding.medium
rect.implicitWidth: Math.max(layout.implicitWidth, placeholder.implicitWidth) + root.padding * 2
rect.implicitHeight: Math.max(layout.implicitHeight, placeholder.implicitHeight) + root.padding * 2
content.opacity: 1
}
}
transitions: Transition {
Anim {
properties: "rightMargin,implicitWidth"
}
Anim {
properties: "topMargin,implicitHeight"
easing: Tokens.anim.expressiveFastSpatial
}
Anim {
property: "opacity"
type: Anim.DefaultEffects
}
}
Behavior on implicitWidth {
Anim {}
}
Behavior on implicitHeight {
Anim {}
}
Item {
id: content
anchors.fill: parent
clip: true
opacity: 0
state: Lyrics.loading || !Lyrics.hasLyrics ? "" : "hasLyrics"
states: State {
name: "hasLyrics"
PropertyChanges {
layout.opacity: 1
placeholder.opacity: 0
}
}
transitions: [
Transition {
from: "hasLyrics"
SequentialAnimation {
Anim {
target: layout
property: "opacity"
type: Anim.FastEffects
}
Anim {
target: placeholder
property: "opacity"
type: Anim.DefaultEffects
}
}
},
Transition {
to: "hasLyrics"
SequentialAnimation {
Anim {
target: placeholder
property: "opacity"
type: Anim.FastEffects
}
Anim {
target: layout
property: "opacity"
type: Anim.DefaultEffects
}
}
}
]
ColumnLayout {
id: layout
anchors.centerIn: parent
spacing: Tokens.spacing.extraSmall
opacity: 0
CustomText {
// TRANSLATORS: %1 = lyrics backend name, e.g. LRCLIB
text: qsTr("Backend: %1").arg(ZUtils.enumToString(Lyrics, "backend"))
color: Colors.palette.m3onSurfaceVariant
animate: true
}
CustomText {
Layout.maximumWidth: 1000 / 2
// TRANSLATORS: %1/%2/%3 = matched track title, artist and album
text: qsTr("Selected candidate: %1 | %2 | %3").arg(Lyrics.selectedCandidate.title).arg(Lyrics.selectedCandidate.artist).arg(Lyrics.selectedCandidate.album)
color: Colors.palette.m3onSurfaceVariant
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
animate: true
}
CustomText {
// TRANSLATORS: %1 = lyric timing offset; ms is the millisecond unit, leave untranslated
text: qsTr("Offset: %1 ms").arg(Lyrics.offset)
color: Colors.palette.m3onSurfaceVariant
animate: true
}
}
Item {
id: placeholder
anchors.centerIn: parent
implicitWidth: placeholderText.implicitWidth
implicitHeight: placeholderText.implicitHeight
CustomText {
id: placeholderText
text: Lyrics.loading ? qsTr("Loading...") : qsTr("No lyrics found")
color: Colors.palette.m3onSurfaceVariant
font: Tokens.font.body.medium
animate: true
}
}
}
}
MouseArea {
id: btn
anchors.centerIn: parent
implicitWidth: implicitHeight
implicitHeight: icon.implicitHeight + Tokens.padding.extraSmall * 2
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onClicked: root.open = !root.open
MaterialIcon {
id: icon
anchors.centerIn: parent
text: "more_vert"
fontStyle: Tokens.font.icon.medium
}
}
}
@@ -0,0 +1,220 @@
pragma Singleton
import QtQuick
import Quickshell
import ZShell.Config
import ZShell.Services
Singleton {
id: root
readonly property var columnDefs: [
{
id: "command",
title: qsTr("Command"),
width: 200,
field: 3
},
{
id: "pid",
title: qsTr("PID"),
width: 60,
field: 1
},
{
id: "cpuUsage",
title: qsTr("CPU %"),
width: 70,
field: 7
},
{
id: "memoryUsage",
title: qsTr("Memory"),
width: 90,
field: 8
},
{
id: "elapsed",
title: qsTr("Elapsed"),
width: 80,
field: 9
},
{
id: "ppid",
title: qsTr("PPID"),
width: 60,
field: 2
},
{
id: "user",
title: qsTr("User"),
width: 100,
field: 11
},
{
id: "uid",
title: qsTr("UID"),
width: 60,
field: 10
},
{
id: "state",
title: qsTr("State"),
width: 60,
field: 5
},
{
id: "sleeping",
title: qsTr("Sleeping"),
width: 80,
field: 6
},
{
id: "commandLine",
title: qsTr("Command line"),
width: 240,
field: 4
},
{
id: "ioReadBytes",
title: qsTr("IO read"),
width: 90,
field: 12
},
{
id: "ioWriteBytes",
title: qsTr("IO write"),
width: 90,
field: 13
}
]
readonly property var desktopEntries: DesktopEntries.applications.values.map(e => ({
icon: e.icon,
command: e.command
}))
Component.onCompleted: {
Processes.setDesktopEntries(root.desktopEntries);
Qt.callLater(ensurePinnedFirst);
}
onDesktopEntriesChanged: Processes.setDesktopEntries(root.desktopEntries)
readonly property string pinnedColumnId: "command"
readonly property var hiddenColumns: {
const visible = Config.dashboard.processColumns.values.map(e => e.id);
return root.columnDefs.filter(d => !visible.includes(d.id));
}
function pinnedIndex(): int {
const values = Config.dashboard.processColumns.values;
for (let i = 0; i < values.length; i++)
if (values[i].id === root.pinnedColumnId)
return i;
return -1;
}
function ensurePinnedFirst() {
const i = root.pinnedIndex();
if (i > 0)
Qt.callLater(() => {
const idx = root.pinnedIndex();
if (idx > 0)
Config.dashboard.processColumns.move(idx, 0);
});
}
function defFor(id: string): var {
for (const def of root.columnDefs)
if (def.id === id)
return def;
return null;
}
function addColumn(id: string) {
const def = root.defFor(id);
Config.dashboard.processColumns.insert({
id: id,
width: def ? def.width : 100
});
}
function removeColumn(index: int) {
const entry = Config.dashboard.processColumns.at(index);
if (entry && entry.id === root.pinnedColumnId)
return;
Config.dashboard.processColumns.remove(index);
}
function moveColumns(from: int, to: int) {
const list = Config.dashboard.processColumns;
const entry = list.at(from);
if (!entry)
return;
if (entry.id === root.pinnedColumnId)
return;
if (to === 0 && list.at(0)?.id === root.pinnedColumnId)
return;
list.move(from, to);
const i = root.pinnedIndex();
if (i > 0)
list.move(i, 0);
}
function resizeColumn(index: int, width: real) {
const entry = Config.dashboard.processColumns.at(index);
if (entry)
entry.width = Math.max(50, width);
}
function dampOvershoot(overshoot: real, maxOvershoot: real): real {
return overshoot / (1 + overshoot / maxOvershoot);
}
function formatCell(id: string, value: var): string {
switch (id) {
case "cpuUsage":
return value.toFixed(1) + "%";
case "memoryUsage":
return root.formatBytes(value * 1024);
case "ioReadBytes":
case "ioWriteBytes":
return root.formatBytes(value);
case "elapsed":
return root.formatElapsed(value);
case "sleeping":
return value ? qsTr("Yes") : qsTr("No");
default:
return String(value);
}
}
function formatBytes(bytes: var): string {
if (bytes === undefined || bytes === null || bytes <= 0)
return "0 B";
const units = ["B", "KiB", "MiB", "GiB", "TiB", "PiB"];
let v = Number(bytes);
let i = 0;
while (v >= 1024 && i < units.length - 1) {
v /= 1024;
++i;
}
return (i === 0 ? v.toFixed(0) : v.toFixed(1)) + " " + units[i];
}
function formatElapsed(secs: var): string {
const s = Math.max(0, Math.floor(Number(secs)));
const h = Math.floor(s / 3600);
const m = Math.floor((s % 3600) / 60);
const ss = s % 60;
if (h > 0)
return h + "h " + m + "m";
if (m > 0)
return m + "m " + ss + "s";
return ss + "s";
}
}
@@ -0,0 +1,665 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import ZShell.Config
import ZShell.Internal
import ZShell.Services
import qs.Components
import qs.Services
Item {
id: root
required property int headerHeight
required property ProcessesList pList
readonly property alias columnList: columnList
readonly property alias columnsModel: columnsData
readonly property bool anyHeld: columnList.contentItem.children.some(c => c.held)
z: 1
implicitHeight: headerHeight
function widthForColumn(id: string, configured: real): real {
for (const item of columnList.contentItem.children) {
if (item.modelData !== undefined && item.modelData.id === id)
return item.liveWidth > 0 ? item.liveWidth : (configured > 0 ? configured : (ProcessesColumns.defFor(id)?.width ?? 100));
}
return configured > 0 ? configured : (ProcessesColumns.defFor(id)?.width ?? 100);
}
function fillMissingWidths() {
const n = columnsData.values.length;
if (n === 0 || columnList.width <= 0)
return;
const spacingTotal = columnList.spacing * Math.max(0, n - 1);
let savedTotal = 0;
const unsavedIndices = [];
for (let i = 0; i < n; i++) {
const w = columnsData.values[i].width;
if (w > 0)
savedTotal += w;
else
unsavedIndices.push(i);
}
if (unsavedIndices.length === 0)
return;
const available = columnList.width - spacingTotal - savedTotal;
if (available <= 0)
return;
const each = Math.max(50, available / unsavedIndices.length);
for (const idx of unsavedIndices)
ProcessesColumns.resizeColumn(idx, each);
}
Connections {
target: columnList
function onWidthChanged() {
root.fillMissingWidths();
}
function onCountChanged() {
resizeOverlay.geometryTick++;
root.fillMissingWidths();
}
function onContentWidthChanged() {
resizeOverlay.geometryTick++;
}
}
Connections {
target: columnsData
function onValuesChanged() {
root.fillMissingWidths();
resizeOverlay.geometryTick++;
}
}
Component.onCompleted: Qt.callLater(fillMissingWidths)
Component.onDestruction: Cursor.clearOverride()
CustomListView {
id: columnList
anchors.fill: parent
orientation: ListView.Horizontal
interactive: false
spacing: Tokens.spacing.extraSmall / 2
add: Transition {
Anim {
from: 0
properties: "opacity"
to: 1
type: Anim.DefaultEffects
}
}
displaced: Transition {
Anim {
properties: "opacity"
to: 1
type: Anim.DefaultEffects
}
Anim {
properties: "x"
}
}
move: Transition {
Anim {
properties: "opacity"
to: 1
type: Anim.DefaultEffects
}
Anim {
properties: "x"
}
}
remove: Transition {
Anim {
to: 0
properties: "opacity"
type: Anim.DefaultEffects
}
}
model: DelegateModel {
id: columnsVisual
delegate: ColumnHeaderCell {
parentModel: columnsVisual
}
model: ScriptModel {
id: columnsData
values: Config.dashboard.processColumns.values
}
}
}
Item {
id: resizeOverlay
property real gapBoost: pressed ? Tokens.spacing.small : Tokens.spacing.extraSmall
property bool pressed: false
property int geometryTick: 0
property int activeGapIndex: -1
anchors.fill: columnList
z: 10
Component.onCompleted: Qt.callLater(() => geometryTick++)
Repeater {
model: Math.max(0, columnsData.values.length - 1)
MouseArea {
id: handleRoot
required property int index
readonly property bool hovered: containsMouse
property ColumnHeaderCell leftCell: {
const left = leftCell;
if (resizing && left)
return left;
const tick = resizeOverlay.geometryTick;
return resolveCell(true);
}
property ColumnHeaderCell rightCell: {
const right = rightCell;
if (resizing && right)
return right;
const tick = resizeOverlay.geometryTick;
return resolveCell(false);
}
property bool resizing: false
property real originX: 0
property real originWidth: 0
property real originRightWidth: 0
property real originAbsX: 0
function resolveCell(left: bool): ColumnHeaderCell {
if (left) { // qmllint disable incompatible-type
const leftItem = columnList.itemAtIndex(index);
if (leftItem)
return leftItem;
else
return null;
} else {
const rightItem = columnList.itemAtIndex(index + 1);
if (rightItem)
return rightItem;
else
return null;
}
}
cursorShape: pressed ? Qt.ClosedHandCursor : Qt.OpenHandCursor
preventStealing: true
hoverEnabled: true
enabled: !root.anyHeld
x: {
if (!leftCell)
return 0;
const gapCenter = leftCell.x + leftCell.width + columnList.spacing / 2;
return gapCenter - width / 2;
}
y: 0
implicitWidth: 16
implicitHeight: columnList.height
function pairAvailableWidth(rightCell) {
let othersWidth = 0;
for (const item of columnList.contentItem.children) {
if (item.modelData === undefined || item === handleRoot.leftCell || item === rightCell)
continue;
othersWidth += item.width;
}
const spacingTotal = columnList.spacing * Math.max(0, columnsData.values.length - 1);
return columnList.width - othersWidth - spacingTotal;
}
CustomRect {
id: handle
readonly property bool shouldBeVisible: handleRoot.hovered || handleRoot.pressed
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
implicitWidth: shouldBeVisible ? resizeOverlay.gapBoost : 0
color: Colors.palette.m3secondary
radius: Tokens.rounding.full
opacity: shouldBeVisible ? 1.0 : 0
Behavior on implicitWidth {
Anim {
type: Anim.FastSpatial
}
}
Behavior on opacity {
Anim {}
}
transform: Scale {
origin {
x: handle.implicitWidth / 2
y: handle.height / 2
}
yScale: handle.shouldBeVisible ? 1 : 0.5
xScale: handle.shouldBeVisible ? 1 : 0.5
Behavior on xScale {
Anim {}
}
Behavior on yScale {
Anim {}
}
}
}
onHoveredChanged: resizeOverlay.activeGapIndex = hovered || pressed ? handleRoot.index : (resizeOverlay.activeGapIndex === handleRoot.index ? -1 : resizeOverlay.activeGapIndex)
onPressedChanged: {
resizeOverlay.pressed = pressed;
if (!pressed)
resizeOverlay.activeGapIndex = hovered ? handleRoot.index : resizeOverlay.activeGapIndex === handleRoot.index ? -1 : resizeOverlay.activeGapIndex;
}
onPressed: e => {
if (!handleRoot.leftCell)
return;
Cursor.setOverride(Qt.ClosedHandCursor);
resizing = true;
originAbsX = handleRoot.mapToItem(resizeOverlay, e.x, 0).x;
originWidth = handleRoot.leftCell.implicitWidth;
originRightWidth = handleRoot.rightCell ? handleRoot.rightCell.implicitWidth : 0;
}
onPositionChanged: e => {
if (!resizing || !handleRoot.leftCell)
return;
const nowAbsX = handleRoot.mapToItem(resizeOverlay, e.x, 0).x;
let newLeftWidth = Math.max(50, originWidth + (nowAbsX - originAbsX));
const rightCell = handleRoot.rightCell;
let roundedLeft = Math.round(newLeftWidth);
if (rightCell) {
const available = Math.round(handleRoot.pairAvailableWidth(rightCell));
const maxLeftWidth = Math.max(50, available - 50);
roundedLeft = Math.min(roundedLeft, maxLeftWidth);
rightCell.liveWidth = Math.max(50, available - roundedLeft);
}
handleRoot.leftCell.liveWidth = roundedLeft;
}
onReleased: {
Cursor.clearOverride();
resizing = false;
const cell = handleRoot.leftCell;
const rightCell = handleRoot.rightCell;
if (cell.liveWidth !== 0)
ProcessesColumns.resizeColumn(cell.DelegateModel.itemsIndex, cell.liveWidth);
if (rightCell.liveWidth !== 0)
ProcessesColumns.resizeColumn(rightCell.DelegateModel.itemsIndex, rightCell.liveWidth);
if (cell || rightCell) {
cell.liveWidth = 0;
rightCell.liveWidth = 0;
}
resizeOverlay.geometryTick++;
}
}
}
}
component ColumnHeaderCell: Item {
id: cell
required property var modelData
required property int index
required property DelegateModel parentModel
property int leftRadius: pushSide !== 1 && DelegateModel.itemsIndex === 0 ? cell.implicitHeight / 2 : Tokens.rounding.extraSmall
property int rightRadius: pushSide !== -1 && DelegateModel.itemsIndex === parentModel.count - 1 ? cell.implicitHeight / 2 : Tokens.rounding.extraSmall
property real radiusLerpProg
property bool held
property bool moved: false
property int pressIndex: -1
property point pressPos
property real lastMoveX: 0
property real liveWidth: 0
readonly property bool isActiveSort: Processes.sortField === (ProcessesColumns.defFor(cell.modelData.id)?.field ?? 0)
readonly property int pushSide: {
const gapIdx = resizeOverlay.activeGapIndex;
if (gapIdx < 0)
return 0;
const myIdx = cell.DelegateModel.itemsIndex;
if (myIdx === gapIdx)
return -1;
if (myIdx === gapIdx + 1)
return 1;
return 0;
}
property real gapBoost: resizeOverlay.gapBoost
readonly property real gapShrink: gapBoost / 2
property real gapProg: pushSide !== 0 ? 1 : 0
property real gapX: pushSide === 1 ? 1 : 0
function lerpRadius(a: real, b: real): real {
return a + (b - a) * radiusLerpProg;
}
Behavior on gapBoost {
Anim {
type: Anim.FastSpatial
}
}
Behavior on gapX {
Anim {
type: Anim.FastSpatial
}
}
Behavior on gapProg {
Anim {
type: Anim.FastSpatial
}
}
implicitHeight: root.headerHeight
implicitWidth: liveWidth > 0 ? liveWidth : (modelData.width > 0 ? modelData.width : (ProcessesColumns.defFor(modelData.id)?.width ?? 100))
z: held || returnAnim.running ? 1 : 0
states: State {
name: "held"
PropertyChanges {
cellBg.color: Colors.palette.m3primaryContainer
elevation.opacity: 1
placeholder.opacity: 0.1
cell.radiusLerpProg: 1
title.color: Colors.palette.m3onPrimaryContainer
removeBtn.inactiveOnColor: Colors.palette.m3onPrimaryContainer
}
}
Behavior on leftRadius {
Anim {
type: Anim.DefaultEffects
}
}
Behavior on rightRadius {
Anim {
type: Anim.DefaultEffects
}
}
transitions: Transition {
Anim {
properties: "opacity,radiusLerpProg"
type: Anim.SlowEffects
}
PropertyAction {
properties: "color,inactiveOnColor"
}
}
state: held ? "held" : ""
CustomRect {
id: placeholder
anchors.fill: parent
color: Colors.palette.m3primary
opacity: 0
topLeftRadius: cell.leftRadius
bottomLeftRadius: cell.leftRadius
topRightRadius: cell.rightRadius
bottomRightRadius: cell.rightRadius
}
DragHandler {
id: dragHandler
property real centerOffset: 0
enabled: cell.modelData.id !== ProcessesColumns.pinnedColumnId
target: null
xAxis.enabled: true
dragThreshold: 10
grabPermissions: PointerHandler.CanTakeOverFromItems
onActiveChanged: {
if (active) {
Cursor.setOverride(Qt.ClosedHandCursor);
returnAnim.stop();
cell.pressIndex = cell.DelegateModel.itemsIndex;
cell.lastMoveX = cell.x;
cell.moved = true;
cell.held = true;
const p = centroid.position;
cell.pressPos = Qt.point(p.x, p.y);
dragHandler.centerOffset = p.x - cell.width / 2;
cellContent.x = Qt.binding(() => {
const maxOvershoot = Tokens.padding.extraLarge;
const xDiff = cell.x - cell.lastMoveX;
const x = dragHandler.centroid.position.x - cell.pressPos.x - xDiff;
const absX = cell.mapToItem(columnList.contentItem, x, 0).x;
const maxX = columnList.contentWidth - cell.width;
if (absX < 0)
return x - absX - ProcessesColumns.dampOvershoot(-absX, maxOvershoot);
if (absX > maxX)
return x - absX + maxX + ProcessesColumns.dampOvershoot(absX - maxX, maxOvershoot);
return x;
});
cellContent.y = Qt.binding(() => {
const maxOvershoot = Tokens.padding.extraLargeIncreased;
const y = dragHandler.centroid.position.y - cell.pressPos.y;
return ProcessesColumns.dampOvershoot(Math.abs(y), maxOvershoot) * Math.sign(y);
});
} else {
Cursor.clearOverride();
if (cell.held) {
cellContent.x = cellContent.x;
cellContent.y = cellContent.y;
cell.held = false;
returnAnim.start();
if (cell.DelegateModel.itemsIndex !== cell.pressIndex) {
ProcessesColumns.moveColumns(cell.pressIndex, cell.DelegateModel.itemsIndex);
}
}
}
}
onCentroidChanged: {
if (!active)
return;
const p = centroid.position;
cell.lastMoveX = cell.x;
const cursorAbsX = cell.mapToItem(columnList.contentItem, p.x, 0).x;
const centerAbsX = cursorAbsX - dragHandler.centerOffset;
const movingRight = p.x > cell.pressPos.x;
const grabbedRight = dragHandler.centerOffset > 0;
const useCursor = movingRight === grabbedRight;
const refX = useCursor ? cursorAbsX : centerAbsX;
const myIndex = cell.DelegateModel.itemsIndex;
const rightItem = columnList.itemAtIndex(myIndex + 1);
if (rightItem) {
const rightMid = rightItem.x + rightItem.width / 2;
if (refX > rightMid) {
columnsVisual.items.move(myIndex, myIndex + 1);
return;
}
}
const leftItem = columnList.itemAtIndex(myIndex - 1);
if (leftItem) {
const leftMid = leftItem.x + leftItem.width / 2;
const pinnedAtZero = columnList.itemAtIndex(0)?.modelData?.id === ProcessesColumns.pinnedColumnId;
if (refX < leftMid && !(myIndex === 1 && pinnedAtZero)) {
columnsVisual.items.move(myIndex, myIndex - 1);
}
}
}
}
Anim {
id: returnAnim
properties: "x,y"
target: cellContent
to: 0
}
Item {
id: cellVisual
x: cell.gapShrink * cell.gapX
y: 0
width: cell.width - cell.gapShrink * cell.gapProg
height: cell.height
Item {
id: cellContent
width: cellVisual.width
height: cellVisual.height
readonly property int topLeftRadius: cell.lerpRadius(cell.leftRadius, cell.implicitHeight / 2)
readonly property int topRightRadius: cell.lerpRadius(cell.rightRadius, cell.implicitHeight / 2)
readonly property int bottomLeftRadius: cell.lerpRadius(cell.leftRadius, cell.implicitHeight / 2)
readonly property int bottomRightRadius: cell.lerpRadius(cell.rightRadius, cell.implicitHeight / 2)
Elevation {
id: elevation
anchors.fill: parent
level: 3
opacity: 0
topLeftRadius: parent.topLeftRadius
topRightRadius: parent.topRightRadius
bottomLeftRadius: parent.bottomLeftRadius
bottomRightRadius: parent.bottomRightRadius
}
CustomRect {
id: cellBg
anchors.fill: parent
color: cell.isActiveSort ? Colors.palette.m3primary : Colors.palette.m3surfaceContainer
topLeftRadius: cell.lerpRadius(cell.leftRadius, cell.implicitHeight / 2)
topRightRadius: cell.lerpRadius(cell.rightRadius, cell.implicitHeight / 2)
bottomLeftRadius: cell.lerpRadius(cell.leftRadius, cell.implicitHeight / 2)
bottomRightRadius: cell.lerpRadius(cell.rightRadius, cell.implicitHeight / 2)
StateLayer {
id: layer
color: cell.isActiveSort ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
manualHoverOverride: cell.held
onClicked: {
Processes.sortBy(ProcessesColumns.defFor(cell.modelData.id)?.field ?? 0);
root.pList.positionViewAtBeginning();
}
}
}
Item {
anchors.fill: parent
anchors.leftMargin: Tokens.padding.medium
anchors.rightMargin: 12
CustomText {
id: title
anchors.left: parent.left
anchors.right: removeBtn.visible ? removeBtn.left : parent.right
anchors.rightMargin: Tokens.spacing.extraSmall
anchors.verticalCenter: parent.verticalCenter
color: cell.isActiveSort ? Colors.palette.m3onPrimary : Colors.palette.m3onSurfaceVariant
elide: CustomText.ElideRight
text: ProcessesColumns.defFor(cell.modelData.id)?.title ?? cell.modelData.id
}
IconButton {
id: removeBtn
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
type: IconButton.Text
icon: "close"
font: Tokens.font.icon.small
inactiveOnColor: Colors.palette.m3onPrimary
visible: cell.modelData.id !== ProcessesColumns.pinnedColumnId
onClicked: ProcessesColumns.removeColumn(cell.DelegateModel.itemsIndex)
}
MaterialIcon {
anchors.right: removeBtn.visible ? removeBtn.left : parent.right
anchors.verticalCenter: parent.verticalCenter
text: "keyboard_arrow_down"
opacity: cell.isActiveSort ? 1 : 0
visible: opacity > 0
rotation: !Processes.sortAscending && opacity > 0 ? 180 : 0
color: Colors.palette.m3onPrimary
Behavior on rotation {
Anim {}
}
Behavior on opacity {
Anim {}
}
}
}
}
}
}
component HiddenColumnItem: MenuItem {
required property var modelData
text: modelData.title
}
}
@@ -0,0 +1,140 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import ZShell.Config
import ZShell.Services
import qs.Components
import qs.Services
CustomListView {
id: root
required property ProcessesHeader columnHeader
required property int rowHeight
property real rowSpacing: Tokens.spacing.small
model: Processes.list
spacing: root.rowSpacing
clip: true
CustomScrollBar.vertical: CustomScrollBar {
flickable: root
}
delegate: CustomRect {
id: processRow
required property var modelData
required property int index
implicitWidth: ListView.view.width
implicitHeight: rowLayout.implicitHeight
radius: Tokens.rounding.full
StateLayer {
id: rowHover
}
RowLayout {
id: rowLayout
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
spacing: Tokens.spacing.extraSmall / 2
CustomClippingRect {
id: iconClip
Layout.fillHeight: true
Layout.margins: Tokens.padding.medium
implicitWidth: height
radius: Tokens.rounding.full
IconImage {
id: appIcon
anchors.fill: parent
implicitSize: height
source: Quickshell.iconPath(processRow.modelData.iconName, "application-x-executable")
}
}
Repeater {
id: cellRepeater
model: root.columnHeader.columnsModel
Item {
id: rowCell
required property var modelData
required property int index
implicitWidth: root.columnHeader.widthForColumn(modelData.id, modelData.width) - (index === 0 ? iconClip.implicitWidth + iconClip.Layout.margins * 2 + rowLayout.spacing : 0)
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
Layout.preferredWidth: implicitWidth
Layout.fillHeight: true
CustomRect {
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.margins: Tokens.padding.small
anchors.leftMargin: -(rowLayout.spacing)
implicitWidth: 2
color: Colors.palette.m3outlineVariant
visible: rowCell.index !== 0
}
Item {
id: layout
anchors.fill: parent
anchors.margins: Tokens.padding.small
anchors.leftMargin: rowCell.index !== 0 ? Tokens.padding.medium : 0
anchors.rightMargin: Tokens.padding.medium
implicitHeight: mainText.implicitHeight + subText.implicitHeight + mainText.anchors.margins * 2 + subText.anchors.topMargin
CustomText {
id: mainText
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
elide: CustomText.ElideRight
text: {
const value = processRow.modelData[rowCell.modelData.id];
return value === undefined ? "" : ProcessesColumns.formatCell(rowCell.modelData.id, value);
}
states: State {
name: "centerAnchored"
when: !subText.visible
AnchorChanges {
target: mainText
anchors.top: undefined
anchors.verticalCenter: layout.verticalCenter
}
}
}
CustomText {
id: subText
anchors.top: mainText.bottom
anchors.left: parent.left
anchors.right: parent.right
visible: rowCell.modelData.id === "command"
elide: CustomText.ElideRight
text: rowCell.modelData.id === "command" ? processRow.modelData["commandLine"] : ""
font: Tokens.font.label.small
color: Colors.palette.m3outline
}
}
}
}
}
}
}
@@ -0,0 +1,86 @@
pragma Singleton
import QtQuick
import Quickshell
import ZShell.Internal
import ZShell.Services
Singleton {
id: root
readonly property int historyLength: 30
readonly property CircularBuffer cpuBuffer: _cpuBuffer
readonly property CircularBuffer memoryBuffer: _memoryBuffer
readonly property list<var> cpuInfo: [
{
label: qsTr("Usage"),
value: `${Math.round(Cpu.percentage * 100)}%`
},
{
label: qsTr("Temp"),
value: `${Math.ceil(Cpu.temperature)}°${"C"}`
}
]
readonly property list<var> memoryInfo: [
{
label: qsTr("Used"),
value: memoryUsageFormat(Memory.used, Memory.total)
},
{
label: qsTr("Cached"),
value: memoryTotalFormat(Memory.cached)
},
{
label: qsTr("Writes"),
value: memoryTotalFormat(Memory.writeTotal)
},
{
label: qsTr("Reads"),
value: memoryTotalFormat(Memory.readTotal)
}
]
function memoryTotalFormat(kib: int): string {
const fmt = UsageFmt.formatKib(kib);
return `${fmt.value.toFixed(1)} ${fmt.unit}`;
}
function memoryUsageFormat(used: int, total: int): string {
const fmt = UsageFmt.formatKib(used, total);
return `${fmt.value.toFixed(1)} / ${Math.floor(fmt.total)} ${fmt.unit}`;
}
Connections {
function onUpdateTriggered(): void {
if (Cpu.percentage >= 0 && isFinite(Cpu.percentage))
_cpuBuffer.push(Cpu.percentage);
}
target: Cpu
}
Connections {
function onUpdateTriggered(): void {
if (Memory.percentage >= 0 && isFinite(Memory.percentage))
_memoryBuffer.push(Memory.percentage);
}
target: Memory
}
CircularBuffer {
id: _cpuBuffer
capacity: root.historyLength + 1
smoothingRadius: 2
}
CircularBuffer {
id: _memoryBuffer
capacity: root.historyLength + 1
smoothingRadius: 2
}
}
+187
View File
@@ -0,0 +1,187 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import ZShell.Config
import ZShell.Components
import ZShell.Services
import qs.Components
import qs.Services
Item {
id: root
required property int radius
implicitHeight: layout.implicitHeight
ServiceRef {
service: Cpu
}
ServiceRef {
service: Memory
}
RowLayout {
id: layout
anchors.fill: parent
spacing: Tokens.spacing.medium
UsageCard {
accent: Colors.palette.m3primary
buffer: ResourceUsage.cpuBuffer
icon: "memory"
label: qsTr("CPU")
values: ResourceUsage.cpuInfo
}
UsageCard {
accent: Colors.palette.m3tertiary
buffer: ResourceUsage.memoryBuffer
icon: "memory_alt"
label: qsTr("Memory")
values: ResourceUsage.memoryInfo
}
}
component UsageCard: CustomRect {
id: card
property color accent
required property var buffer
property string icon
property string label
property list<var> values
color: Colors.tPalette.m3surfaceContainer
radius: root.radius
Layout.fillWidth: true
Layout.fillHeight: true
implicitHeight: mainLayout.implicitHeight + mainLayout.anchors.margins * 2
ColumnLayout {
id: mainLayout
anchors.fill: parent
anchors.margins: Tokens.padding.large
spacing: 0
RowLayout {
spacing: Tokens.spacing.small
MaterialIcon {
color: card.accent
text: card.icon
}
CustomText {
text: card.label
}
}
Item {
id: graphWrapper
Layout.fillHeight: true
Layout.fillWidth: true
implicitHeight: infoColumn.implicitHeight
Item {
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: infoColumn.left
SparklineItem {
id: sparkline
anchors.fill: parent
historyLength: ResourceUsage.historyLength
line1: card.buffer
line1Color: card.accent
line1FillAlpha: 0.15
maxValue: 1.0
slideProgress: 1
Connections {
function onValuesChanged(): void {
slideAnim.restart();
}
target: card.buffer
}
NumberAnimation {
id: slideAnim
duration: Config.dashboard.resourceUpdateInterval
easing.type: Easing.Linear
from: 0
property: "slideProgress"
target: sparkline
to: 1
}
}
// "Collecting data" placeholder
CustomText {
anchors.centerIn: parent
color: Colors.palette.m3outline
text: qsTr("Collecting data...")
visible: card.buffer.count < 2
}
}
ColumnLayout {
id: infoColumn
anchors.right: parent.right
anchors.bottom: parent.bottom
width: {
const prefWidth = graphWrapper.width / 3;
const minWidth = graphWrapper.width / 4;
if (prefWidth < implicitWidth)
return implicitWidth;
else if (prefWidth >= implicitWidth * 2)
return minWidth;
else
return prefWidth;
}
Repeater {
model: card.values
Item {
id: moreInfo
required property var modelData
Layout.leftMargin: Tokens.spacing.medium
Layout.fillWidth: true
implicitWidth: label.implicitWidth + value.implicitWidth + label.anchors.margins * 2
implicitHeight: label.implicitHeight
CustomText {
id: label
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
color: Colors.palette.m3onSurfaceVariant
text: moreInfo.modelData.label
}
CustomText {
id: value
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
color: card.accent
text: moreInfo.modelData.value
}
}
}
}
}
}
}
}

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