Author SHA1 Message Date
zachandAramJonghu 9bab59ba0f Merge branch 'main' into feat/task-runner-ci
C++ / fmt (pull_request) Successful in 5s
JS/TS / fmt & lint (pull_request) Successful in 17s
Python / fmt & lint (pull_request) Successful in 44s
C++ / build & lint (pull_request) Failing after 2m26s
Python / test & nuitka build (pull_request) Failing after 1m41s
Rust / build, clippy & fmt (pull_request) Failing after 6m24s
2026-10-25 01:34:38 +02:00
AramJonghu 08d0c0e9fc feat(makefile): added qmlformat + install -> compile in cpp step 2026-10-25 01:34:38 +02:00
AramJonghu 82b93e3193 feat(makefile): C++, Python, and Rust checks included (incomplete) 2026-10-25 01:34:38 +02:00
AramJonghu 509b9d1831 feat(makefile): makefile build & install + progress on cpp and py 2026-10-25 01:34:38 +02:00
AramJonghu 2bf6b9cc1f format(clang): make cpp-fmt result 2026-10-25 01:34:38 +02:00
AramJonghu 8cc9ff36e7 ci: rework Dockerfile image + buildx dep added 2026-10-25 01:34:38 +02:00
AramJonghu e25b864982 refactor(workflows): combined steps where it makes sense 2026-10-25 01:34:38 +02:00
AramJonghu 417be33c6a fix(ci): missing dependency wayland-scanner 2026-10-25 01:34:38 +02:00
137 changed files with 1560 additions and 8361 deletions
-3
View File
@@ -1,3 +0,0 @@
#omz revision: d2b7f557cceaddb16ba89bbf8200d8a8df35c4d8
#omz fpath: /home/zach/.oh-my-zsh/plugins/colored-man-pages /home/zach/.oh-my-zsh/plugins/universalarchive /home/zach/.oh-my-zsh/plugins/tldr /home/zach/.oh-my-zsh/plugins/extract /home/zach/.oh-my-zsh/plugins/copypath /home/zach/.oh-my-zsh/plugins/cp /home/zach/.oh-my-zsh/plugins/conda-env /home/zach/.oh-my-zsh/plugins/command-not-found /home/zach/.oh-my-zsh/plugins/dirhistory /home/zach/.oh-my-zsh/plugins/copybuffer /home/zach/.oh-my-zsh/plugins/copyfile /home/zach/.oh-my-zsh/custom/plugins/fast-syntax-highlighting /home/zach/.oh-my-zsh/custom/plugins/zsh-syntax-highlighting /home/zach/.oh-my-zsh/custom/plugins/zsh-autosuggestions /home/zach/.oh-my-zsh/plugins/archlinux /home/zach/.oh-my-zsh/plugins/web-search /home/zach/.oh-my-zsh/plugins/sudo /home/zach/.oh-my-zsh/plugins/git /home/zach/.oh-my-zsh/functions /home/zach/.oh-my-zsh/completions /home/zach/.oh-my-zsh/custom/functions /home/zach/.oh-my-zsh/custom/completions /home/zach/tools/ripgrep/complete /home/linuxbrew/.linuxbrew/share/zsh/site-functions /home/zach/.oh-my-zsh/plugins/colored-man-pages /home/zach/.oh-my-zsh/plugins/universalarchive /home/zach/.oh-my-zsh/plugins/tldr /home/zach/.oh-my-zsh/plugins/extract /home/zach/.oh-my-zsh/plugins/copypath /home/zach/.oh-my-zsh/plugins/cp /home/zach/.oh-my-zsh/plugins/conda-env /home/zach/.oh-my-zsh/plugins/command-not-found /home/zach/.oh-my-zsh/plugins/dirhistory /home/zach/.oh-my-zsh/plugins/copybuffer /home/zach/.oh-my-zsh/plugins/copyfile /home/zach/.oh-my-zsh/custom/plugins/fast-syntax-highlighting /home/zach/.oh-my-zsh/custom/plugins/zsh-syntax-highlighting /home/zach/.oh-my-zsh/custom/plugins/zsh-autosuggestions /home/zach/.oh-my-zsh/plugins/archlinux /home/zach/.oh-my-zsh/plugins/web-search /home/zach/.oh-my-zsh/plugins/sudo /home/zach/.oh-my-zsh/plugins/git /home/zach/.oh-my-zsh/functions /home/zach/.oh-my-zsh/completions /home/zach/.oh-my-zsh/custom/functions /home/zach/.oh-my-zsh/custom/completions /home/zach/.oh-my-zsh/cache/completions /home/zach/tools/ripgrep/complete /home/linuxbrew/.linuxbrew/share/zsh/site-functions /usr/local/share/zsh/site-functions /usr/share/zsh/site-functions /usr/share/zsh/functions/Calendar /usr/share/zsh/functions/Chpwd /usr/share/zsh/functions/Completion /usr/share/zsh/functions/Completion/Base /usr/share/zsh/functions/Completion/Linux /usr/share/zsh/functions/Completion/Unix /usr/share/zsh/functions/Completion/X /usr/share/zsh/functions/Completion/Zsh /usr/share/zsh/functions/Exceptions /usr/share/zsh/functions/MIME /usr/share/zsh/functions/Math /usr/share/zsh/functions/Misc /usr/share/zsh/functions/Newuser /usr/share/zsh/functions/Prompts /usr/share/zsh/functions/TCP /usr/share/zsh/functions/VCS_Info /usr/share/zsh/functions/VCS_Info/Backends /usr/share/zsh/functions/Zftp /usr/share/zsh/functions/Zle /home/zach/.zfunc
+4 -23
View File
@@ -17,7 +17,8 @@ jobs:
run: | run: |
find . \( -name '*.cpp' -o -name '*.h' -o -name '*.hpp' \) \ find . \( -name '*.cpp' -o -name '*.h' -o -name '*.hpp' \) \
-not -path './build/*' \ -not -path './build/*' \
-exec clang-format --dry-run -Werror --style=file {} + -exec clang-format -i --style=file {} +
git diff --exit-code && echo "clang-format: passed"
build & lint: build & lint:
runs-on: alpine runs-on: alpine
@@ -29,35 +30,15 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Cache CMake build
uses: actions/cache@v4
with:
path: build
key: cpp-build-${{ hashFiles('CMakeLists.txt', '**/CMakeLists.txt') }}
restore-keys: |
cpp-build-
- name: Cache ccache
uses: actions/cache@v4
with:
path: ~/.ccache
key: ccache-${{ runner.os }}-${{ github.sha }}
restore-keys: |
ccache-${{ runner.os }}-
- name: Configure - name: Configure
run: cmake -B build -G Ninja -DENABLE_MODULES=plugin -DCMAKE_BUILD_TYPE=Release run: cmake -B build -G Ninja -DENABLE_MODULES=plugin -DCMAKE_BUILD_TYPE=Release
- name: Build - name: Build
run: ninja -C build run: ninja -C build
env:
CMAKE_CXX_COMPILER_LAUNCHER: ccache
- name: clang-tidy - name: clang-tidy
run: | run: |
sed -i 's/-mno-direct-extern-access //g' build/compile_commands.json sed -i 's/-mno-direct-extern-access //g' build/compile_commands.json
set -o pipefail set -o pipefail
run-clang-tidy -p build -j "$(nproc)" \ run-clang-tidy -p build -j $(nproc) -header-filter="Plugins/.*" 2>&1 | \
-source-filter="^$(pwd)/Plugins/" \ grep -v 'Suppressed\|non-user\|non-system\|unknown argument.*mno-direct\|Found compiler error\|warnings generated\.\|^Use -header-filter'
-header-filter=".*Plugins/.*" \
-removed-arg=-mno-direct-extern-access
+58 -51
View File
@@ -3,78 +3,85 @@ name: Rust
on: on:
pull_request: pull_request:
# jobs can be separated if we integrate tests
jobs: jobs:
clippy & fmt: build, clippy & fmt:
runs-on: alpine runs-on: alpine
container: rust:alpine container: node:26-alpine
env: env:
CARGO_HOME: ${{ github.workspace }}/.cargo CARGO_HOME: ${{ github.workspace }}/.cargo
steps: steps:
- name: Install node for actions
run: |
apk add --no-cache nodejs git
- name: Install rust components
run: |
rustup component add clippy rustfmt
cargo clippy --version
rustfmt --version
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Cache cargo packages - name: Cache cargo packages
uses: actions/cache@v4 uses: actions/cache@v4
env:
cache-name: cache-cargo-packages
with: with:
path: | path: |
.cargo/registry .cargo/registry
.cargo/git .cargo/git
target target
key: rust-build-${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} key: rust-build-${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: | restore-keys: |
rust-${{ runner.os }}-cargo- rust-${{ runner.os }}-build-${{ env.cache-name }}-
rust-${{ runner.os }}-build-
rust-
- name: Clippy (with compile check) - name: Install tools
run: | run: |
manifests=$(find . \( -name .cargo -o -name .git -o -name target \) -prune -o -name Cargo.toml -print) apk add --no-cache \
if [ -z "$manifests" ]; then git \
if [ -n "$(find . \( -name .cargo -o -name target \) -prune -o -name '*.rs' -print -quit)" ]; then cargo \
echo "Rust files found but no Cargo.toml" rust \
exit 1 rust-clippy \
fi rustfmt
echo "No Rust project found"
exit 0
fi
status=0
for manifest in $manifests; do
if (cd "$(dirname "$manifest")" && cargo clippy --all-targets --locked -- -D warnings); then
echo "$manifest: Clippy passed"
else
echo "$manifest: Clippy failed"
status=1
fi
done
exit $status
- name: Cargo check
run: |
if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then
for manifest in $(find . -name "Cargo.toml"); do
cargo check --manifest-path "$manifest" && \
echo "$manifest: check passed" || \
{ echo "$manifest: check failed"; exit 1; }
done
elif [ -n "$(find . -name "*.rs" -print -quit)" ]; then
echo "Rust files found but no Cargo.toml"
exit 1
else
echo "No Rust project found"
fi
- name: Clippy
run: |
if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then
status=0
for manifest in $(find . -name "Cargo.toml"); do
cargo clippy --manifest-path "$manifest" --all-targets --all-features -- -D warnings && \
echo "$manifest: Clippy passed" || \
{ echo "$manifest: Clippy failed"; status=1; }
done
exit $status
elif [ -n "$(find . -name "*.rs" -print -quit)" ]; then
echo "Rust files found but no Cargo.toml"
exit 1
else
echo "No Rust project found"
fi
- name: Format check - name: Format check
run: | run: |
manifests=$(find . \( -name .cargo -o -name .git -o -name target \) -prune -o -name Cargo.toml -print) if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then
if [ -z "$manifests" ]; then status=0
if [ -n "$(find . \( -name .cargo -o -name target \) -prune -o -name '*.rs' -print -quit)" ]; then for manifest in $(find . -name "Cargo.toml"); do
echo "Rust files found but no Cargo.toml" cargo fmt --manifest-path "$manifest" --check && \
exit 1 echo "$manifest: formatting OK" || \
fi { echo "$manifest: needs formatting"; status=1; }
done
exit $status
elif [ -n "$(find . -name "*.rs" -print -quit)" ]; then
echo "Rust files found but no Cargo.toml"
exit 1
else
echo "No Rust project found" echo "No Rust project found"
exit 0
fi fi
status=0
for manifest in $manifests; do
if (cd "$(dirname "$manifest")" && cargo fmt --check); then
echo "$manifest: formatting OK"
else
echo "$manifest: needs formatting"
status=1
fi
done
exit $status
+10 -25
View File
@@ -1,38 +1,23 @@
**/__pycache__/
./result/ ./result/
.pyre/ .pyre/
.cache/
.venv/
.qmlls.ini
build/
compile_commands.json
testpython
pkg/ pkg/
*.tar.* *.tar.*
uv.lock
.qtcreator/
dist/ dist/
**/target/ **/target/
**/test-plugins/ **/test-plugins/
**/Charts/ **/Charts/
network-dev/ network-dev/
**/zshell.dist/
# Qml
.qtcreator/
.qmlls.ini
# CMake compile artifacts
.cache/
compile_commands.json
build/
# Cargo home (CI sets CARGO_HOME inside the workspace)
.cargo/
# Python
uv.lock
testpython
.venv/
**/__pycache__/
# Nuitka compile
**/zshell.pyi
**/zshell.cpython-*
**/zshell.build/ **/zshell.build/
**/zshell.dist/
# Agents stuff
.opencode/ .opencode/
run-agent.sh run-agent.sh
AGENTS.md AGENTS.md
-1
View File
@@ -50,7 +50,6 @@ 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(QT_NO_PRIVATE_MODULE_WARNING ON)
set(ENABLE_MODULES "plugin;shell;cli;m3shapes" CACHE STRING "Modules to build/install") set(ENABLE_MODULES "plugin;shell;cli;m3shapes" CACHE STRING "Modules to build/install")
set(INSTALL_LIBDIR "${CMAKE_INSTALL_LIBDIR}/ZShell" CACHE STRING "Library install dir") set(INSTALL_LIBDIR "${CMAKE_INSTALL_LIBDIR}/ZShell" CACHE STRING "Library install dir")
+4 -2
View File
@@ -46,7 +46,8 @@ Item {
strokeWidth: Math.min(1, remainingArc.sweepAngle) * root.strokeWidth strokeWidth: Math.min(1, remainingArc.sweepAngle) * root.strokeWidth
Behavior on strokeColor { Behavior on strokeColor {
CAnim {} CAnim {
}
} }
PathAngleArc { PathAngleArc {
@@ -83,7 +84,8 @@ Item {
} }
} }
Behavior on color { Behavior on color {
CAnim {} CAnim {
}
} }
Anim on waveProgress { Anim on waveProgress {
id: waveProgAnim id: waveProgAnim
+2 -3
View File
@@ -1,5 +1,4 @@
import QtQuick import QtQuick
import Quickshell
import ZShell.Components import ZShell.Components
Item { Item {
@@ -20,7 +19,7 @@ Item {
} }
ShaderEffectSource { ShaderEffectSource {
id: src id: source
hideSource: true hideSource: true
sourceItem: contentItem sourceItem: contentItem
@@ -28,7 +27,7 @@ Item {
RoundedMask { RoundedMask {
anchors.fill: parent anchors.fill: parent
source: src source: source
topLeftRadius: root.topLeftRadius topLeftRadius: root.topLeftRadius
topRightRadius: root.topRightRadius topRightRadius: root.topRightRadius
bottomRightRadius: root.bottomRightRadius bottomRightRadius: root.bottomRightRadius
+2 -1
View File
@@ -160,7 +160,8 @@ Item {
Behavior on color { Behavior on color {
enabled: false enabled: false
CAnim {} CAnim {
}
} }
} }
+2 -1
View File
@@ -8,6 +8,7 @@ MultiEffect {
colorization: 1 colorization: 1
Behavior on colorizationColor { Behavior on colorizationColor {
CAnim {} CAnim {
}
} }
} }
+2 -1
View File
@@ -7,6 +7,7 @@ ClippingRectangle {
color: "transparent" color: "transparent"
Behavior on color { Behavior on color {
CAnim {} CAnim {
}
} }
} }
+2 -1
View File
@@ -7,6 +7,7 @@ ClippingWrapperRectangle {
color: "transparent" color: "transparent"
Behavior on color { Behavior on color {
CAnim {} CAnim {
}
} }
} }
+4 -2
View File
@@ -124,7 +124,8 @@ ProgressBar {
anchors.left: parent.left anchors.left: parent.left
Behavior on implicitWidth { Behavior on implicitWidth {
Anim {} Anim {
}
} }
Component.onCompleted: implicitWidth = Qt.binding(() => parent.width * root.visualPosition) Component.onCompleted: implicitWidth = Qt.binding(() => parent.width * root.visualPosition)
@@ -221,7 +222,8 @@ ProgressBar {
} }
} }
Behavior on color { Behavior on color {
CAnim {} CAnim {
}
} }
Anim on waveProgress { Anim on waveProgress {
duration: root.waveDuration duration: root.waveDuration
+10 -5
View File
@@ -105,16 +105,20 @@ Switch {
width: height width: height
Behavior on end1 { Behavior on end1 {
PropAnim {} PropAnim {
}
} }
Behavior on end2 { Behavior on end2 {
PropAnim {} PropAnim {
}
} }
Behavior on start1 { Behavior on start1 {
PropAnim {} PropAnim {
}
} }
Behavior on start2 { Behavior on start2 {
PropAnim {} PropAnim {
}
} }
ShapePath { ShapePath {
@@ -130,7 +134,8 @@ Switch {
strokeWidth: 16 * 0.15 strokeWidth: 16 * 0.15
Behavior on strokeColor { Behavior on strokeColor {
CAnim {} CAnim {
}
} }
PathLine { PathLine {
+4 -2
View File
@@ -20,7 +20,8 @@ Text {
textFormat: Text.PlainText textFormat: Text.PlainText
Behavior on color { Behavior on color {
CAnim {} CAnim {
}
} }
Behavior on text { Behavior on text {
enabled: root.animate enabled: root.animate
@@ -32,7 +33,8 @@ Text {
type: Anim.FastEffects type: Anim.FastEffects
} }
PropertyAction {} PropertyAction {
}
TAnim { TAnim {
target: root target: root
+2 -1
View File
@@ -65,7 +65,8 @@ Image {
value: root.source value: root.source
} }
PropertyAction {} PropertyAction {
}
ScriptAction { ScriptAction {
script: root.maybeStartInAnim() script: root.maybeStartInAnim()
+1 -4
View File
@@ -138,10 +138,7 @@ Item {
const t = (tlo + thi) / 2; const t = (tlo + thi) / 2;
const u = 1 - t; const u = 1 - t;
const y = u * u * u * p1.y + 3 * u * u * t * c1y + 3 * u * t * t * c2y + t * t * t * p2.y; const y = u * u * u * p1.y + 3 * u * u * t * c1y + 3 * u * t * t * c2y + t * t * t * p2.y;
return { return { x: targetX, y };
x: targetX,
y
};
} }
// Part of the curve left/right of a cut x, including the cut point // Part of the curve left/right of a cut x, including the cut point
+2 -1
View File
@@ -64,7 +64,8 @@ MaterialShape {
to: 360 to: 360
} }
Behavior on color { Behavior on color {
CAnim {} CAnim {
}
} }
ElapsedTimer { ElapsedTimer {
+4 -2
View File
@@ -47,10 +47,12 @@ Item {
implicitWidth: vertical ? elideText.implicitHeight : elideText.implicitWidth implicitWidth: vertical ? elideText.implicitHeight : elideText.implicitWidth
Behavior on leftFadeStrength { Behavior on leftFadeStrength {
Anim {} Anim {
}
} }
Behavior on rightFadeStrength { Behavior on rightFadeStrength {
Anim {} Anim {
}
} }
onHeightChanged: resetMarquee() onHeightChanged: resetMarquee()
+5 -3
View File
@@ -34,7 +34,7 @@ MouseArea {
cursorShape: expanded ? Qt.ArrowCursor : undefined cursorShape: expanded ? Qt.ArrowCursor : undefined
enabled: expanded enabled: expanded
hoverEnabled: expanded hoverEnabled: expanded
layer.enabled: false layer.enabled: opacity < 1
opacity: expanded ? 1 : 0 opacity: expanded ? 1 : 0
parent: { parent: {
const win = QsWindow.window; const win = QsWindow.window;
@@ -101,7 +101,8 @@ MouseArea {
yScale: root.expanded ? 1 : 0.1 yScale: root.expanded ? 1 : 0.1
Behavior on yScale { Behavior on yScale {
Anim {} Anim {
}
} }
} }
@@ -136,7 +137,8 @@ MouseArea {
radius: menu.radius - column.anchors.margins radius: menu.radius - column.anchors.margins
Behavior on radius { Behavior on radius {
Anim {} Anim {
}
} }
StateLayer { StateLayer {
+6 -3
View File
@@ -102,7 +102,8 @@ Row {
topLeftRadius: rad topLeftRadius: rad
Behavior on rad { Behavior on rad {
Anim {} Anim {
}
} }
StateLayer { StateLayer {
@@ -126,10 +127,12 @@ Row {
text: "expand_more" text: "expand_more"
Behavior on anchors.horizontalCenterOffset { Behavior on anchors.horizontalCenterOffset {
Anim {} Anim {
}
} }
Behavior on rotation { Behavior on rotation {
Anim {} Anim {
}
} }
} }
} }
+6 -3
View File
@@ -44,7 +44,8 @@ Item {
onValuesChanged: root.flagChanged() onValuesChanged: root.flagChanged()
} }
ToastWrapper {} ToastWrapper {
}
} }
component ToastWrapper: MouseArea { component ToastWrapper: MouseArea {
@@ -85,10 +86,12 @@ Item {
} }
} }
Behavior on opacity { Behavior on opacity {
Anim {} Anim {
}
} }
Behavior on scale { Behavior on scale {
Anim {} Anim {
}
} }
Component.onCompleted: modelData.lock(this) Component.onCompleted: modelData.lock(this)
+2 -1
View File
@@ -20,7 +20,8 @@ Shape {
strokeWidth: 0 strokeWidth: 0
Behavior on fillColor { Behavior on fillColor {
CAnim {} CAnim {
}
} }
PathSvg { PathSvg {
+1
View File
@@ -141,6 +141,7 @@ Singleton {
BeatTracker { BeatTracker {
id: beatTracker id: beatTracker
} }
PwObjectTracker { PwObjectTracker {
+2 -1
View File
@@ -295,7 +295,8 @@ Singleton {
Component { Component {
id: apComp id: apComp
AccessPoint {} AccessPoint {
}
} }
Process { Process {
+5 -2
View File
@@ -1081,13 +1081,15 @@ Singleton {
Component { Component {
id: commandProc id: commandProc
CommandProcess {} CommandProcess {
}
} }
Component { Component {
id: apComp id: apComp
AccessPoint {} AccessPoint {
}
} }
Timer { Timer {
@@ -1308,6 +1310,7 @@ Singleton {
} }
stdout: StdioCollector { stdout: StdioCollector {
id: stdoutCollector id: stdoutCollector
} }
onExited: code => { onExited: code => {
+2 -1
View File
@@ -164,7 +164,8 @@ Singleton {
Component { Component {
id: notifComp id: notifComp
Notif {} Notif {
}
} }
component Notif: QtObject { component Notif: QtObject {
+4 -2
View File
@@ -46,7 +46,8 @@ Item {
strokeWidth: Math.min(1, remainingArc.sweepAngle) * root.strokeWidth strokeWidth: Math.min(1, remainingArc.sweepAngle) * root.strokeWidth
Behavior on strokeColor { Behavior on strokeColor {
CAnim {} CAnim {
}
} }
PathAngleArc { PathAngleArc {
@@ -83,7 +84,8 @@ Item {
} }
} }
Behavior on color { Behavior on color {
CAnim {} CAnim {
}
} }
Anim on waveProgress { Anim on waveProgress {
id: waveProgAnim id: waveProgAnim
+2 -1
View File
@@ -7,6 +7,7 @@ ClippingRectangle {
color: "transparent" color: "transparent"
Behavior on color { Behavior on color {
CAnim {} CAnim {
}
} }
} }
+4 -2
View File
@@ -20,7 +20,8 @@ Text {
textFormat: Text.PlainText textFormat: Text.PlainText
Behavior on color { Behavior on color {
CAnim {} CAnim {
}
} }
Behavior on text { Behavior on text {
enabled: root.animate enabled: root.animate
@@ -32,7 +33,8 @@ Text {
type: Anim.FastEffects type: Anim.FastEffects
} }
PropertyAction {} PropertyAction {
}
TAnim { TAnim {
target: root target: root
+2 -1
View File
@@ -65,7 +65,8 @@ Image {
value: root.source value: root.source
} }
PropertyAction {} PropertyAction {
}
ScriptAction { ScriptAction {
script: root.maybeStartInAnim() script: root.maybeStartInAnim()
+2 -1
View File
@@ -64,7 +64,8 @@ MaterialShape {
to: 360 to: 360
} }
Behavior on color { Behavior on color {
CAnim {} CAnim {
}
} }
ElapsedTimer { ElapsedTimer {
+1
View File
@@ -200,5 +200,6 @@ Singleton {
ElapsedTimer { ElapsedTimer {
id: timer id: timer
} }
} }
+1
View File
@@ -16,6 +16,7 @@ ShellRoot {
GreeterState { GreeterState {
id: greeter id: greeter
} }
GreeterSurface { GreeterSurface {
+2 -1
View File
@@ -69,7 +69,8 @@ Singleton {
model: Quickshell.screens model: Quickshell.screens
Monitor {} Monitor {
}
} }
Process { Process {
+2 -1
View File
@@ -33,7 +33,8 @@ Singleton {
implicitHeight: 0 implicitHeight: 0
implicitWidth: 0 implicitWidth: 0
mask: Region {} mask: Region {
}
} }
} }
+43 -105
View File
@@ -5,7 +5,7 @@ import Quickshell
import Quickshell.Io import Quickshell.Io
import QtQuick import QtQuick
import ZShell.Models import ZShell.Models
import ZShell.Wallpaper import ZShell.Internal
import ZShell.Config import ZShell.Config
import qs.Components import qs.Components
import qs.Services import qs.Services
@@ -16,48 +16,24 @@ Searcher {
id: root id: root
property string actualCurrent: WallpaperPath.currentWallpaperPath property string actualCurrent: WallpaperPath.currentWallpaperPath
property alias crops: adapter.monitorCrops
readonly property string current: showPreview ? previewPath : actualCurrent readonly property string current: showPreview ? previewPath : actualCurrent
readonly property string currentThumbnail: thumbnailFor(current) property alias monitorCrops: monitorCrops
readonly property bool currentIsVideo: isVideoPath(current)
property bool pendingPreviewClear property bool pendingPreviewClear
property bool previewColorLock property bool previewColorLock
property string previewPath property string previewPath
property bool recentlyChanged property bool recentlyChanged
property bool showPreview: false property bool showPreview: false
property int cropVersion: 0
function getCategoryFor(w: WallpaperEntry): string { function getCategoryFor(w: FileSystemEntry): string {
let category = w.parentDir.slice(Paths.wallsdir.length + 1); let category = w.parentDir.slice(Paths.wallsdir.length + 1);
if (category.includes("/")) if (category.includes("/"))
category = category.slice(0, category.indexOf("/")); category = category.slice(0, category.indexOf("/"));
return category; return category;
} }
readonly property var videoExts: ["mp4", "mkv", "webm", "avi", "mov", "wmv", "flv", "m4v", "mpg", "mpeg", "3gp", "ogv", "ts", "vob"]
function isVideoPath(path: string): bool {
if (!path)
return false;
const dot = path.lastIndexOf(".");
if (dot < 0)
return false;
return videoExts.includes(path.slice(dot + 1).toLowerCase());
}
function thumbnailFor(path: string): string {
if (!path)
return "";
for (const entry of root.list) {
if (entry.path === path)
return entry.thumbnailPath || path;
}
return path;
}
function getCrop(screen: string): var { function getCrop(screen: string): var {
return wallpapers.crop(root.current, screen); return root.crops[screen];
} }
function preview(path: string): void { function preview(path: string): void {
@@ -74,18 +50,19 @@ Searcher {
else if (zoom > 5.0) else if (zoom > 5.0)
zoom = 5.0; zoom = 5.0;
wallpapers.setCrop(root.current, screen, { root.crops[screen] = {
x: rect.x, x: rect.x,
y: rect.y, y: rect.y,
width: rect.width, width: rect.width,
height: rect.height, height: rect.height,
zoom: zoom zoom: zoom
}); };
} }
function setWallpaper(path: string): void { function setWallpaper(path: string): void {
actualCurrent = path; actualCurrent = path;
WallpaperPath.currentWallpaperPath = path; WallpaperPath.currentWallpaperPath = path;
Quickshell.screens.forEach(n => setCrop(n.name, Qt.rect(0, 0, 1, 1), 1.0));
Quickshell.execDetached(["zshell-cli", "wallpaper", "lockscreen", "--input-image", `${root.actualCurrent}`, "--output-path", `${Paths.state}/lockscreen_bg.png`, "--blur-amount", `${Config.lock.blurAmount}`]); Quickshell.execDetached(["zshell-cli", "wallpaper", "lockscreen", "--input-image", `${root.actualCurrent}`, "--output-path", `${Paths.state}/lockscreen_bg.png`, "--blur-amount", `${Config.lock.blurAmount}`]);
if (Config.colors.schemeGen) if (Config.colors.schemeGen)
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--image-path", `${root.actualCurrent}`, "--scheme", `${Config.colors.schemeType}`, "--mode", `${Config.colors.mode}`]); Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--image-path", `${root.actualCurrent}`, "--scheme", `${Config.colors.schemeType}`, "--mode", `${Config.colors.mode}`]);
@@ -119,13 +96,38 @@ Searcher {
target: "wallpaper" target: "wallpaper"
} }
WallpaperDb { FileView {
id: monitorCrops
path: `${Paths.state}/wallpaper-crops.json`
watchChanges: true
onAdapterUpdated: cropWriteDelay.restart()
onFileChanged: reload()
JsonAdapter {
id: adapter
property var monitorCrops: ({})
}
}
Timer {
id: cropWriteDelay
interval: 100
repeat: false
running: false
onTriggered: monitorCrops.writeAdapter()
}
FileSystemModel {
id: wallpapers id: wallpapers
dbPath: `${Paths.state}/wallpapers.sqlite` filter: FileSystemModel.Images
cacheSize: wallpaper.largestBufferSize
requireHardwareCodec: true
path: Config.general.wallpaperPath path: Config.general.wallpaperPath
recursive: true
} }
Process { Process {
@@ -144,100 +146,36 @@ Searcher {
WallpaperService { WallpaperService {
id: wallpaper id: wallpaper
dbPath: wallpapers.dbPath
path: root.current path: root.current
} }
Connections {
target: wallpapers
function onCacheStored(path: string): void {
if (path === root.current)
wallpaper.invalidateCache();
}
function onCropsChanged(path: string): void {
if (path === root.current)
root.cropVersion++;
}
}
Variants { Variants {
id: screenVariants
model: Quickshell.screens model: Quickshell.screens
WallpaperObject { WallpaperObject {
id: obj readonly property var crops: root.getCrop(modelData.name)
required property var modelData required property var modelData
// Crop animation only for edits, never for wallpaper swaps. cropHeight: crops?.height ?? 1.0
property bool animateCrop: false cropWidth: crops?.width ?? 1.0
cropX: crops?.x ?? 0.0
cropY: crops?.y ?? 0.0
screen: modelData.name screen: modelData.name
service: wallpaper service: wallpaper
zoom: crops?.zoom ?? 1.0
// Apply the wallpaper's own crop. Driven off the service's pathChanged
// (which fires after applyPath has snapshotted the outgoing frame with
// the outgoing crop) and off crop edits, never as a reactive binding on
// the wallpaper path — so a swap applies the incoming crop atomically
// after the base is captured.
function seedCrop(): void {
// Load the current wallpaper's crop without pushing: the service
// already read it from the database. This only seeds the values so
// a later edit animates from the right starting point.
obj.syncCrop(root.getCrop(modelData.name));
}
// Apply an edited crop: assign via the setters so the Behaviors animate
// the numbers and push each intermediate value to the service.
function editCrop(): void {
const c = root.getCrop(modelData.name);
obj.cropX = c?.x ?? 0.0;
obj.cropY = c?.y ?? 0.0;
obj.cropWidth = c?.width ?? 1.0;
obj.cropHeight = c?.height ?? 1.0;
obj.zoom = c?.zoom ?? 1.0;
}
Component.onCompleted: seedCrop()
onScreenChanged: seedCrop()
Behavior on cropHeight { Behavior on cropHeight {
enabled: obj.animateCrop
Anim {} Anim {}
} }
Behavior on cropWidth { Behavior on cropWidth {
enabled: obj.animateCrop
Anim {} Anim {}
} }
Behavior on cropX { Behavior on cropX {
enabled: obj.animateCrop
Anim {} Anim {}
} }
Behavior on cropY { Behavior on cropY {
enabled: obj.animateCrop
Anim {} Anim {}
} }
} }
} }
Connections {
target: wallpaper
function onPathChanged(): void {
for (const obj of screenVariants.instances) {
obj.animateCrop = false;
obj.seedCrop();
}
}
}
Connections {
target: root
function onCropVersionChanged(): void {
for (const obj of screenVariants.instances) {
obj.animateCrop = true;
obj.editCrop();
}
}
}
} }
+2 -1
View File
@@ -210,7 +210,8 @@ GridLayout {
roleValue: "network" roleValue: "network"
delegate: EntryWrapper { delegate: EntryWrapper {
NetworkWidget {} NetworkWidget {
}
} }
} }
+1 -3
View File
@@ -45,9 +45,7 @@ WidgetBase {
} }
required property GridLayout loader required property GridLayout loader
required property Wrapper popouts required property Wrapper popouts
readonly property int roundedWidth: Math.round(timeText.implicitWidth) readonly property real size: horizontal ? timeText.contentWidth + Tokens.padding.medium * 2 : verticalColumn.implicitHeight + Tokens.padding.medium * 2
readonly property int evenWidth: roundedWidth % 2 === 0 ? roundedWidth : roundedWidth + 1
readonly property real size: horizontal ? evenWidth + Tokens.padding.medium * 2 : verticalColumn.implicitHeight + Tokens.padding.medium * 2
required property ScreenState screenState required property ScreenState screenState
implicitHeight: horizontal ? baseSize : size implicitHeight: horizontal ? baseSize : size
@@ -24,7 +24,7 @@ Row {
color: Battery.getColors(root.percentage, root.devState).bg color: Battery.getColors(root.percentage, root.devState).bg
height: root.batHeight height: root.batHeight
radius: root.radius radius: root.radius
width: root.batWidth - 1 width: root.batWidth
CustomText { CustomText {
color: Battery.getColors(root.percentage, root.devState).fg color: Battery.getColors(root.percentage, root.devState).fg
+1 -1
View File
@@ -13,7 +13,7 @@ WidgetBase {
readonly property int textWidth: Math.min(metrics.width, 200) readonly property int textWidth: Math.min(metrics.width, 200)
implicitHeight: horizontal ? baseSize : layout.implicitHeight + Tokens.padding.small * 2 implicitHeight: horizontal ? baseSize : layout.implicitHeight + Tokens.padding.small * 2
implicitWidth: horizontal ? (layout.implicitWidth % 2 === 0 ? layout.implicitWidth : layout.implicitWidth + 1) + Tokens.padding.small * 2 : baseSize implicitWidth: horizontal ? layout.implicitWidth + Tokens.padding.small * 2 : baseSize
radius: Tokens.rounding.full radius: Tokens.rounding.full
Behavior on implicitHeight { Behavior on implicitHeight {
+12 -8
View File
@@ -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,12 +23,17 @@ GridLayout {
property bool warning: percentage * 100 >= warningThreshold property bool warning: percentage * 100 >= warningThreshold
property int warningThreshold: 80 property int warningThreshold: 80
function snap(v) {
return Math.round(v * dpr) / dpr;
}
columnSpacing: Tokens.spacing.medium columnSpacing: Tokens.spacing.medium
columns: horizontal ? -1 : 1 columns: horizontal ? -1 : 1
percentage: 0 percentage: 0
Behavior on animatedPercentage { Behavior on animatedPercentage {
Anim {} Anim {
}
} }
Component.onCompleted: animatedPercentage = percentage Component.onCompleted: animatedPercentage = percentage
@@ -53,7 +59,7 @@ GridLayout {
Layout.preferredHeight: root.horizontal ? icon.implicitHeight - Tokens.padding.extraSmall : 4 Layout.preferredHeight: root.horizontal ? icon.implicitHeight - Tokens.padding.extraSmall : 4
Layout.preferredWidth: root.horizontal ? 4 : icon.implicitWidth Layout.preferredWidth: root.horizontal ? 4 : icon.implicitWidth
color: Colors.layer(Colors.palette.m3surfaceContainerHigh, 2) color: Colors.layer(Colors.palette.m3surfaceContainerHigh, 2)
radius: Tokens.rounding.full radius: Math.min(width, height) / 2
Item { Item {
id: clipper id: clipper
@@ -63,20 +69,18 @@ GridLayout {
anchors.left: root.horizontal ? undefined : parent.left anchors.left: root.horizontal ? undefined : parent.left
anchors.verticalCenter: root.horizontal ? undefined : parent.verticalCenter anchors.verticalCenter: root.horizontal ? undefined : parent.verticalCenter
clip: true clip: true
implicitHeight: root.horizontal ? root.percentage * track.height : track.height * 2 height: root.horizontal ? root.snap(root.percentage * track.height) : track.height * 2
implicitWidth: root.horizontal ? track.width * 2 : root.percentage * track.width width: root.horizontal ? track.width * 2 : root.snap(root.percentage * track.width)
CustomRect { CustomRect {
anchors.bottom: root.horizontal ? parent.bottom : undefined anchors.bottom: root.horizontal ? parent.bottom : undefined
anchors.horizontalCenter: root.horizontal ? parent.horizontalCenter : undefined anchors.horizontalCenter: root.horizontal ? parent.horizontalCenter : undefined
anchors.left: root.horizontal ? undefined : parent.left anchors.left: root.horizontal ? undefined : parent.left
anchors.verticalCenter: root.horizontal ? undefined : parent.verticalCenter anchors.verticalCenter: root.horizontal ? undefined : parent.verticalCenter
color: root.mainColor color: root.mainColor
height: track.height
radius: track.radius radius: track.radius
width: track.width
implicitHeight: track.height
implicitWidth: track.width
} }
} }
} }
-2
View File
@@ -33,7 +33,6 @@ WidgetBase {
anchors.horizontalCenterOffset: root.horizontal ? 0 : 1 anchors.horizontalCenterOffset: root.horizontal ? 0 : 1
anchors.verticalCenterOffset: root.horizontal ? 0 : -3 anchors.verticalCenterOffset: root.horizontal ? 0 : -3
columnSpacing: Tokens.spacing.small columnSpacing: Tokens.spacing.small
rowSpacing: 0
columns: root.horizontal ? -1 : 1 columns: root.horizontal ? -1 : 1
ServiceRef { ServiceRef {
@@ -52,7 +51,6 @@ WidgetBase {
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
Layout.fillHeight: root.horizontal Layout.fillHeight: root.horizontal
Layout.fillWidth: !root.horizontal Layout.fillWidth: !root.horizontal
horizontal: root.horizontal horizontal: root.horizontal
icon: "memory" icon: "memory"
iconColor: root.screenState.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface iconColor: root.screenState.resources ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
+9 -8
View File
@@ -31,7 +31,7 @@ WidgetBase {
return -1; return -1;
} }
readonly property real size: horizontal ? grid.implicitWidth + grid.anchors.leftMargin + grid.anchors.rightMargin : grid.implicitHeight + grid.anchors.bottomMargin + grid.anchors.topMargin readonly property real size: horizontal ? grid.implicitWidth + grid.anchors.leftMargin + grid.anchors.rightMargin : grid.implicitHeight + grid.anchors.bottomMargin + grid.anchors.topMargin
readonly property int spacing: Tokens.spacing.extraSmall readonly property int spacing: Tokens.spacing.medium / 2
// Entries that can shrink to nothing, spacing included // Entries that can shrink to nothing, spacing included
function collapsed(entry: var): bool { function collapsed(entry: var): bool {
@@ -51,12 +51,14 @@ 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 {
}
} }
GridLayout { GridLayout {
@@ -68,8 +70,6 @@ WidgetBase {
anchors.rightMargin: root.horizontal ? Tokens.padding.extraSmall : 0 anchors.rightMargin: root.horizontal ? Tokens.padding.extraSmall : 0
anchors.topMargin: root.horizontal ? 0 : Tokens.padding.extraSmall anchors.topMargin: root.horizontal ? 0 : Tokens.padding.extraSmall
columns: root.horizontal ? -1 : 1 columns: root.horizontal ? -1 : 1
columnSpacing: root.spacing
rowSpacing: root.spacing
Repeater { Repeater {
model: ScriptModel { model: ScriptModel {
@@ -111,7 +111,8 @@ WidgetBase {
delegate: EntryWrapper { delegate: EntryWrapper {
name: "upower" name: "upower"
UPowerWidget {} UPowerWidget {
}
} }
} }
} }
@@ -134,8 +135,8 @@ WidgetBase {
Layout.rightMargin: root.horizontal ? Math.round(rightGap) : 0 Layout.rightMargin: root.horizontal ? Math.round(rightGap) : 0
Layout.topMargin: root.horizontal ? 0 : Math.round(leftGap) Layout.topMargin: root.horizontal ? 0 : Math.round(leftGap)
children: item children: item
implicitHeight: item?.height ?? item?.implicitHeight ?? 0 implicitHeight: item?.implicitHeight ?? 0
implicitWidth: item?.width ?? item?.implicitWidth ?? 0 implicitWidth: item?.implicitWidth ?? 0
Behavior on leftGap { Behavior on leftGap {
Anim { Anim {
@@ -6,7 +6,7 @@ import qs.Components
import qs.Services import qs.Services
MaterialIcon { MaterialIcon {
id: root id: speaker
required property bool horizontal required property bool horizontal
@@ -15,8 +15,6 @@ MaterialIcon {
fill: 1 fill: 1
fontStyle: Tokens.font.icon.small fontStyle: Tokens.font.icon.small
text: Audio.muted ? "volume_off" : "volume_up" text: Audio.muted ? "volume_off" : "volume_up"
width: Math.round(implicitWidth)
height: Math.round(implicitHeight)
Behavior on Layout.maximumWidth { Behavior on Layout.maximumWidth {
Anim {} Anim {}
@@ -6,7 +6,7 @@ import qs.Components
import qs.Services import qs.Services
MaterialIcon { MaterialIcon {
id: root id: mic
required property bool horizontal required property bool horizontal
@@ -15,8 +15,6 @@ MaterialIcon {
fill: 1 fill: 1
fontStyle: Tokens.font.icon.small fontStyle: Tokens.font.icon.small
text: Audio.sourceMuted ? "mic_off" : "mic" text: Audio.sourceMuted ? "mic_off" : "mic"
width: Math.round(implicitWidth)
height: Math.round(implicitHeight)
Behavior on Layout.maximumWidth { Behavior on Layout.maximumWidth {
Anim {} Anim {}
-1
View File
@@ -38,7 +38,6 @@ WidgetBase {
anchors.topMargin: root.horizontal ? 0 : Tokens.padding.extraSmall anchors.topMargin: root.horizontal ? 0 : Tokens.padding.extraSmall
columns: root.horizontal ? -1 : 1 columns: root.horizontal ? -1 : 1
rowSpacing: -2 rowSpacing: -2
columnSpacing: Tokens.spacing.extraSmall
Repeater { Repeater {
id: repeater id: repeater
+27 -25
View File
@@ -12,14 +12,13 @@ import qs.Services
Item { Item {
id: root id: root
property real activeWorkspaceMargin: Tokens.padding.extraSmall / 2 property real activeWorkspaceMargin: Math.ceil(Tokens.padding.medium / 4)
readonly property int spacing: Tokens.spacing.extraSmall / 2
readonly property int effectiveActiveWorkspaceId: monitor?.activeWorkspace?.id ?? 1 readonly property int effectiveActiveWorkspaceId: monitor?.activeWorkspace?.id ?? 1
required property bool horizontal required property bool horizontal
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen) readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen)
required property ShellScreen screen required property ShellScreen screen
readonly property real shortSize: Math.max(Config.bar.height, Tokens.sizes.bar.innerSize) readonly property real shortSize: Math.max(Config.bar.height, Tokens.sizes.bar.innerSize)
readonly property real size: (workspaceButtonWidth * workspacesShown) + (spacing * Math.max(0, workspacesShown - 1)) + 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
readonly property list<var> workspaces: Hyprland.workspaces.values.filter(w => w.monitor === root.monitor) readonly property list<var> workspaces: Hyprland.workspaces.values.filter(w => w.monitor === root.monitor)
@@ -60,17 +59,15 @@ Item {
anchors.fill: parent anchors.fill: parent
anchors.margins: root.activeWorkspaceMargin anchors.margins: root.activeWorkspaceMargin
columnSpacing: root.spacing columnSpacing: 0
columns: root.horizontal ? root.workspacesShown : 1 columns: root.horizontal ? root.workspacesShown : 1
rowSpacing: root.spacing rowSpacing: 0
rows: root.horizontal ? 1 : root.workspacesShown rows: root.horizontal ? 1 : root.workspacesShown
layer.enabled: true
layer.mipmap: true
Repeater { Repeater {
model: root.workspaces model: root.workspaces
CustomRect { Button {
id: button id: button
required property int index required property int index
@@ -78,19 +75,27 @@ Item {
implicitHeight: indicator.indicatorThickness implicitHeight: indicator.indicatorThickness
implicitWidth: indicator.indicatorThickness implicitWidth: indicator.indicatorThickness
radius: Tokens.rounding.full width: root.workspaceButtonWidth
CustomText { background: Item {
anchors.centerIn: parent id: workspaceButtonBackground
color: Colors.palette.m3onSecondaryContainer
text: button.modelData?.name ?? "" implicitHeight: root.workspaceButtonWidth
implicitWidth: root.workspaceButtonWidth
CustomText {
anchors.centerIn: parent
color: Colors.palette.m3onSecondaryContainer
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
text: button.modelData?.name ?? ""
verticalAlignment: Text.AlignVCenter
}
} }
StateLayer { onPressed: {
onPressed: { const ws = button.modelData.name;
const ws = button.modelData.name; Hyprland.dispatch(Hyprland.usingLua ? `hl.dsp.focus({ workspace= "${ws}"})` : `workspace ${ws}`);
Hyprland.dispatch(Hyprland.usingLua ? `hl.dsp.focus({ workspace= "${ws}"})` : `workspace ${ws}`);
}
} }
} }
} }
@@ -99,10 +104,8 @@ Item {
CustomRect { CustomRect {
id: indicator id: indicator
readonly property real step: root.workspaceButtonWidth + root.spacing property real indicatorLength: (Math.abs(idxPair.idx1 - idxPair.idx2) + 1) * root.workspaceButtonWidth
property real indicatorPosition: Math.min(idxPair.idx1, idxPair.idx2) * root.workspaceButtonWidth + root.activeWorkspaceMargin
property real indicatorLength: (Math.abs(idxPair.idx1 - idxPair.idx2) + 1) * root.workspaceButtonWidth + Math.abs(idxPair.idx1 - idxPair.idx2) * root.spacing
property real indicatorPosition: Math.min(idxPair.idx1, idxPair.idx2) * step + root.activeWorkspaceMargin
property real indicatorThickness: root.workspaceButtonWidth property real indicatorThickness: root.workspaceButtonWidth
anchors.horizontalCenter: root.horizontal ? undefined : parent.horizontalCenter anchors.horizontalCenter: root.horizontal ? undefined : parent.horizontalCenter
@@ -126,10 +129,9 @@ Item {
implicitHeight: grid.height implicitHeight: grid.height
implicitWidth: grid.width implicitWidth: grid.width
source: grid source: grid
autoPaddingEnabled: false
sourceColor: Colors.palette.m3onSurface sourceColor: Colors.palette.m3onSurface
x: root.horizontal ? -indicator.x + root.activeWorkspaceMargin : 0 x: root.horizontal ? -indicator.x + 3 : 0
y: root.horizontal ? 0 : -indicator.y + root.activeWorkspaceMargin y: root.horizontal ? 0 : -indicator.y + 3
} }
} }
} }
+2 -1
View File
@@ -50,7 +50,8 @@ Item {
} }
Behavior on offsetScale { Behavior on offsetScale {
Anim {} Anim {
}
} }
Behavior on x { Behavior on x {
enabled: !root.horizontal || root.offsetScale < 1 enabled: !root.horizontal || root.offsetScale < 1
+14 -7
View File
@@ -26,15 +26,20 @@ StackView {
initialItem: SubMenu { initialItem: SubMenu {
handle: root.trayItem handle: root.trayItem
} }
popEnter: NoAnim {} popEnter: NoAnim {
popExit: NoAnim {} }
pushEnter: NoAnim {} popExit: NoAnim {
pushExit: NoAnim {} }
pushEnter: NoAnim {
}
pushExit: NoAnim {
}
Component { Component {
id: subMenuComp id: subMenuComp
SubMenu {} SubMenu {
}
} }
component NoAnim: Transition { component NoAnim: Transition {
@@ -54,10 +59,12 @@ StackView {
spacing: Tokens.spacing.extraSmall spacing: Tokens.spacing.extraSmall
Behavior on opacity { Behavior on opacity {
Anim {} Anim {
}
} }
Behavior on scale { Behavior on scale {
Anim {} Anim {
}
} }
Component.onCompleted: shown = true Component.onCompleted: shown = true
+2 -4
View File
@@ -54,7 +54,7 @@ Item {
} }
} }
CustomRect { ClipRect {
id: preview id: preview
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
@@ -64,7 +64,6 @@ Item {
color: Colors.tPalette.m3surfaceContainer color: Colors.tPalette.m3surfaceContainer
implicitWidth: ClipHistory.previewIsImage ? Math.max(Math.min(imagePreview.sourceSize.width + Tokens.padding.large * 2, Config.clipboard.sizes.previewWidth), Config.clipboard.sizes.minPreviewWidth) : Math.max(Math.min(textPreviewColumn.width + textPreviewColumn.anchors.margins * 2, Config.clipboard.sizes.previewWidth), Config.clipboard.sizes.minPreviewWidth) implicitWidth: ClipHistory.previewIsImage ? Math.max(Math.min(imagePreview.sourceSize.width + Tokens.padding.large * 2, Config.clipboard.sizes.previewWidth), Config.clipboard.sizes.minPreviewWidth) : Math.max(Math.min(textPreviewColumn.width + textPreviewColumn.anchors.margins * 2, Config.clipboard.sizes.previewWidth), Config.clipboard.sizes.minPreviewWidth)
radius: 25 radius: 25
clip: true
Behavior on implicitWidth { Behavior on implicitWidth {
Anim {} Anim {}
@@ -229,12 +228,11 @@ Item {
anchors.fill: parent anchors.fill: parent
spacing: Tokens.spacing.small spacing: Tokens.spacing.small
Item { ClipRect {
id: textRect id: textRect
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
clip: true
Item { Item {
id: textWrapper id: textWrapper
+4 -2
View File
@@ -307,10 +307,12 @@ CustomMouseArea {
} }
} }
Behavior on x { Behavior on x {
Anim {} Anim {
}
} }
Behavior on y { Behavior on y {
Anim {} Anim {
}
} }
onTodayItemChanged: { onTodayItemChanged: {
+2 -1
View File
@@ -77,7 +77,8 @@ Item {
implicitSize: height implicitSize: height
Behavior on clampedVal { Behavior on clampedVal {
Anim {} Anim {
}
} }
MaterialIcon { MaterialIcon {
+4 -2
View File
@@ -33,7 +33,8 @@ Item {
Repeater { Repeater {
id: shapes id: shapes
DriftingShape {} DriftingShape {
}
} }
FrameAnimation { FrameAnimation {
@@ -79,7 +80,8 @@ Item {
shape: root.shapePool[Math.floor(Math.random() * root.shapePool.length)] shape: root.shapePool[Math.floor(Math.random() * root.shapePool.length)]
Behavior on color { Behavior on color {
CAnim {} CAnim {
}
} }
Behavior on opacity { Behavior on opacity {
Anim { Anim {
+2 -1
View File
@@ -18,7 +18,8 @@ Item {
layer.enabled: true layer.enabled: true
Behavior on fallbackColor { Behavior on fallbackColor {
CAnim {} CAnim {
}
} }
layer.effect: MultiEffect { layer.effect: MultiEffect {
blurMax: 1 blurMax: 1
+10 -5
View File
@@ -54,12 +54,14 @@ Item {
Behavior on x { Behavior on x {
enabled: !mouseArea.drag.active && !root.isSnapping && !root.iconsRoot.selectedIcons.includes(root.filePath) enabled: !mouseArea.drag.active && !root.isSnapping && !root.iconsRoot.selectedIcons.includes(root.filePath)
Anim {} Anim {
}
} }
Behavior on y { Behavior on y {
enabled: !mouseArea.drag.active && !root.isSnapping && !root.iconsRoot.selectedIcons.includes(root.filePath) enabled: !mouseArea.drag.active && !root.isSnapping && !root.iconsRoot.selectedIcons.includes(root.filePath)
Anim {} Anim {
}
} }
Item { Item {
@@ -83,7 +85,8 @@ Item {
y: (root.iconsRoot.selectedIcons.includes(root.filePath) && root.iconsRoot.dragLeader !== "" && root.iconsRoot.dragLeader !== root.filePath) ? root.iconsRoot.groupDragY : 0 y: (root.iconsRoot.selectedIcons.includes(root.filePath) && root.iconsRoot.dragLeader !== "" && root.iconsRoot.dragLeader !== root.filePath) ? root.iconsRoot.groupDragY : 0
} }
transitions: Transition { transitions: Transition {
Anim {} Anim {
}
} }
onXChanged: { onXChanged: {
@@ -205,7 +208,8 @@ Item {
radius: Tokens.rounding.small radius: Tokens.rounding.small
Behavior on opacity { Behavior on opacity {
Anim {} Anim {
}
} }
} }
@@ -264,7 +268,8 @@ Item {
radius: Tokens.rounding.small radius: Tokens.rounding.small
Behavior on opacity { Behavior on opacity {
Anim {} Anim {
}
} }
} }
} }
+2 -1
View File
@@ -82,7 +82,8 @@ CustomRect {
Loader { Loader {
active: isSeparator active: isSeparator
sourceComponent: DockSeparator {} sourceComponent: DockSeparator {
}
anchors { anchors {
fill: parent fill: parent
+2 -1
View File
@@ -41,7 +41,8 @@ CarouselView {
tileHeight: focalWidth / 16 * 9 tileHeight: focalWidth / 16 * 9
delegate: Component { delegate: Component {
WallpaperTile {} WallpaperTile {
}
} }
Component.onDestruction: Wallpapers.stopPreview() Component.onDestruction: Wallpapers.stopPreview()
+8 -60
View File
@@ -1,5 +1,3 @@
pragma ComponentBehavior: Bound
import Quickshell import Quickshell
import QtQuick import QtQuick
import ZShell.Config import ZShell.Config
@@ -11,7 +9,7 @@ Item {
id: root id: root
required property bool isCurrent required property bool isCurrent
required property var modelData required property var modelData // FileSystemEntry-like {path, relativePath}
required property real tileHeight required property real tileHeight
required property real tileWidth required property real tileWidth
required property ScreenState screenState required property ScreenState screenState
@@ -31,75 +29,25 @@ Item {
radius: Tokens.rounding.small radius: Tokens.rounding.small
y: Tokens.padding.large y: Tokens.padding.large
Loader { MaterialIcon {
active: opacity > 0
anchors.centerIn: parent anchors.centerIn: parent
opacity: img.status === Image.Ready ? 0 : 1 color: Colors.tPalette.m3outline
fontStyle: Tokens.font.icon.builders.extraLarge.scale(2).weight(Font.DemiBold).build()
Behavior on opacity { text: "image"
Anim {}
}
sourceComponent: CustomRect {
color: Colors.palette.m3primaryContainer
implicitHeight: loadingIndicator.implicitSize + Tokens.padding.large * 2
implicitWidth: loadingIndicator.implicitSize + Tokens.padding.large * 2
radius: Tokens.rounding.full
LoadingIndicator {
id: loadingIndicator
anchors.centerIn: parent
containsIcon: true
implicitSize: Math.min(image.width, image.height) * 0.3
}
}
} }
Image { CachingImage {
id: img
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top anchors.top: parent.top
asynchronous: true cache: true
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
height: root.tileHeight height: root.tileHeight
source: root.modelData ? root.modelData.thumbnailPath : "" path: root.modelData ? root.modelData.path : ""
smooth: root.isCurrent smooth: root.isCurrent
sourceSize.height: root.tileHeight sourceSize.height: root.tileHeight
sourceSize.width: root.tileHeight * (16 / 9) sourceSize.width: root.tileHeight * (16 / 9)
width: root.tileHeight * (16 / 9) width: root.tileHeight * (16 / 9)
opacity: status === Image.Ready ? 1 : 0
Behavior on opacity {
Anim {
type: Anim.SlowEffects
}
}
Loader {
opacity: root.modelData.isVideo && img.status === Image.Ready ? 1 : 0
active: opacity > 0
anchors.centerIn: parent
Behavior on opacity {
Anim {
type: Anim.SlowEffects
}
}
sourceComponent: CustomRect {
implicitWidth: videoIcon.implicitWidth + Tokens.padding.extraLarge * 2
implicitHeight: implicitWidth
radius: Tokens.rounding.full
color: Colors.tPalette.m3surface
MaterialIcon {
id: videoIcon
anchors.centerIn: parent
text: "video_library"
fontStyle: Tokens.font.icon.extraLarge
}
}
}
} }
StateLayer { StateLayer {
+2 -1
View File
@@ -22,7 +22,8 @@ Searcher {
model: Config.launcher.actions.values.filter(a => (a.enabled ?? true) && (Config.launcher.enableDangerousActions || !(a.dangerous ?? false))) model: Config.launcher.actions.values.filter(a => (a.enabled ?? true) && (Config.launcher.enableDangerousActions || !(a.dangerous ?? false)))
Action {} Action {
}
} }
component Action: QtObject { component Action: QtObject {
+17 -4
View File
@@ -5,13 +5,14 @@ import qs.Helpers
import ZShell.Config import ZShell.Config
import qs.Services import qs.Services
Item { ClipRect {
id: root id: root
required property var lock required property var lock
implicitHeight: layout.implicitHeight implicitHeight: layout.implicitHeight
implicitWidth: layout.implicitWidth implicitWidth: layout.implicitWidth
radius: Tokens.rounding.largeIncreased
RowLayout { RowLayout {
id: layout id: layout
@@ -49,10 +50,22 @@ Item {
lock: root.lock lock: root.lock
} }
NotifDock { ColumnLayout {
Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
lock: root.lock spacing: Tokens.spacing.medium
CustomRect {
Layout.fillHeight: true
Layout.fillWidth: true
bottomRightRadius: Tokens.rounding.largeIncreased
color: Colors.tPalette.m3surfaceContainer
radius: Tokens.rounding.small
topRightRadius: Tokens.rounding.largeIncreased
NotifDock {
lock: root.lock
}
}
} }
} }
} }
+1 -1
View File
@@ -221,9 +221,9 @@ WlSessionLockSurface {
anchors.fill: parent anchors.fill: parent
color: Colors.palette.m3surface color: Colors.palette.m3surface
layer.enabled: true
opacity: Colors.transparency.enabled ? Colors.transparency.base : 1 opacity: Colors.transparency.enabled ? Colors.transparency.base : 1
radius: lockContent.radius radius: lockContent.radius
layer.enabled: true
layer.effect: MultiEffect { layer.effect: MultiEffect {
blurMax: 15 blurMax: 15
+26 -31
View File
@@ -7,44 +7,39 @@ import qs.Components
import qs.Helpers import qs.Helpers
import qs.Services import qs.Services
Item { ClipRect {
id: root id: root
required property var lock required property var lock
color: Colors.tPalette.m3surfaceContainer
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2 implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
radius: Tokens.rounding.small
FadeImage {
id: image
ClipRect {
anchors.fill: parent anchors.fill: parent
asynchronous: true
fillMode: Image.PreserveAspectCrop
layer.enabled: true
opacity: status === Image.Ready ? 1 : 0
source: Players.getArtUrl(Players.active)
sourceSize: {
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
return Qt.size(width * dpr, height * dpr);
}
color: Colors.tPalette.m3surfaceContainer Behavior on opacity {
radius: Tokens.rounding.small Anim {
bottomLeftRadius: Tokens.rounding.largeIncreased type: Anim.StandardExtraLarge
}
FadeImage { }
id: image
CustomRect {
anchors.fill: parent anchors.fill: parent
asynchronous: true color: Colors.palette.m3surface
fillMode: Image.PreserveAspectCrop opacity: 0.7
opacity: status === Image.Ready ? 1 : 0
source: Players.getArtUrl(Players.active)
sourceSize: {
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
return Qt.size(width * dpr, height * dpr);
}
Behavior on opacity {
Anim {
type: Anim.StandardExtraLarge
}
}
CustomRect {
anchors.fill: parent
color: Colors.palette.m3surface
opacity: 0.7
}
} }
} }
@@ -82,7 +77,7 @@ Item {
spacing: Tokens.spacing.extraSmall spacing: Tokens.spacing.extraSmall
IconButton { IconButton {
enabled: Players.active?.canGoPrevious ?? false enabled: Players.active?.canGoPrevious
icon: "skip_previous" icon: "skip_previous"
isRound: true isRound: true
shapeMorph: true shapeMorph: true
@@ -93,7 +88,7 @@ Item {
IconButton { IconButton {
checked: Players.active?.isPlaying ?? false checked: Players.active?.isPlaying ?? false
enabled: Players.active?.canTogglePlaying ?? false enabled: Players.active?.canTogglePlaying
icon: Players.active?.isPlaying ? "pause" : "play_arrow" icon: Players.active?.isPlaying ? "pause" : "play_arrow"
implicitWidth: implicitHeight + Tokens.padding.largeIncreased * 2 implicitWidth: implicitHeight + Tokens.padding.largeIncreased * 2
isRound: true isRound: true
@@ -103,7 +98,7 @@ Item {
} }
IconButton { IconButton {
enabled: Players.active?.canGoNext ?? false enabled: Players.active?.canGoNext
icon: "skip_next" icon: "skip_next"
isRound: true isRound: true
shapeMorph: true shapeMorph: true
+101 -91
View File
@@ -11,116 +11,126 @@ import ZShell.Config
import qs.Daemons import qs.Daemons
import qs.Services import qs.Services
CustomRect { ColumnLayout {
id: root id: root
required property var lock required property var lock
bottomRightRadius: Tokens.rounding.largeIncreased anchors.fill: parent
color: Colors.tPalette.m3surfaceContainer anchors.margins: Tokens.padding.large
radius: Tokens.rounding.small spacing: Tokens.spacing.small
topRightRadius: Tokens.rounding.largeIncreased
ColumnLayout { CustomText {
anchors.fill: parent Layout.fillWidth: true
anchors.margins: Tokens.padding.large color: Colors.palette.m3outline
spacing: Tokens.spacing.small elide: Text.ElideRight
font: Tokens.font.mono.builders.small.weight(Font.Medium).build()
text: NotifServer.list.length > 0 ? qsTr("%1 notification%2").arg(NotifServer.list.length).arg(NotifServer.list.length === 1 ? "" : "s") : qsTr("Notifications")
}
CustomText { ClipRect {
Layout.fillWidth: true id: clipRect
color: Colors.palette.m3outline
elide: Text.ElideRight
font: Tokens.font.mono.builders.small.weight(Font.Medium).build()
text: NotifServer.list.length > 0 ? qsTr("%1 notification%2").arg(NotifServer.list.length).arg(NotifServer.list.length === 1 ? "" : "s") : qsTr("Notifications")
}
Item { Layout.fillHeight: true
Layout.fillHeight: true Layout.fillWidth: true
Layout.fillWidth: true color: "transparent"
radius: Tokens.rounding.small
Loader { Loader {
active: opacity > 0 active: opacity > 0
anchors.centerIn: parent anchors.centerIn: parent
opacity: NotifServer.list.length > 0 ? 0 : 1 opacity: NotifServer.list.length > 0 ? 0 : 1
Behavior on opacity { Behavior on opacity {
Anim { Anim {
duration: Tokens.anim.durations.extraLarge duration: Tokens.anim.durations.extraLarge
}
}
sourceComponent: ColumnLayout {
spacing: Tokens.spacing.largeIncreased
CustomText {
Layout.alignment: Qt.AlignHCenter
color: Colors.palette.m3outlineVariant
font: Tokens.font.headline.builders.small.width(90).build()
text: qsTr("No Notifications")
}
} }
} }
sourceComponent: ColumnLayout {
spacing: Tokens.spacing.largeIncreased
CustomListView { Image {
anchors.fill: parent asynchronous: true
clip: true fillMode: Image.PreserveAspectFit
spacing: Tokens.spacing.small layer.enabled: true
source: Qt.resolvedUrl(`${Quickshell.shellDir}/assets/dino.png`)
sourceSize.width: clipRect.width * 0.8
add: Transition { layer.effect: Coloriser {
Anim { brightness: 1
from: 0 colorizationColor: Colors.palette.m3outlineVariant
property: "opacity"
to: 1
}
Anim {
duration: Tokens.anim.durations.expressiveDefaultSpatial
easing: Tokens.anim.expressiveDefaultSpatial
from: 0
property: "scale"
to: 1
} }
} }
delegate: NotifGroup {}
displaced: Transition {
Anim {
properties: "opacity,scale"
to: 1
}
Anim { CustomText {
duration: Tokens.anim.durations.expressiveDefaultSpatial Layout.alignment: Qt.AlignHCenter
easing: Tokens.anim.expressiveDefaultSpatial color: Colors.palette.m3outlineVariant
property: "y" font: Tokens.font.headline.builders.small.width(90).build()
} text: qsTr("No Notifications")
} }
model: ScriptModel { }
values: { }
const list = NotifServer.notClosed.map(n => [n.appName, null]);
return [...new Map(list).keys()];
}
}
move: Transition {
Anim {
properties: "opacity,scale"
to: 1
}
Anim { CustomListView {
duration: Tokens.anim.durations.expressiveDefaultSpatial anchors.fill: parent
easing: Tokens.anim.expressiveDefaultSpatial clip: true
property: "y" spacing: Tokens.spacing.small
}
}
remove: Transition {
Anim {
property: "opacity"
to: 0
}
Anim { add: Transition {
property: "scale" Anim {
to: 0.6 from: 0
} property: "opacity"
to: 1
}
Anim {
duration: Tokens.anim.durations.expressiveDefaultSpatial
easing: Tokens.anim.expressiveDefaultSpatial
from: 0
property: "scale"
to: 1
}
}
delegate: NotifGroup {}
displaced: Transition {
Anim {
properties: "opacity,scale"
to: 1
}
Anim {
duration: Tokens.anim.durations.expressiveDefaultSpatial
easing: Tokens.anim.expressiveDefaultSpatial
property: "y"
}
}
model: ScriptModel {
values: {
const list = NotifServer.notClosed.map(n => [n.appName, null]);
return [...new Map(list).keys()];
}
}
move: Transition {
Anim {
properties: "opacity,scale"
to: 1
}
Anim {
duration: Tokens.anim.durations.expressiveDefaultSpatial
easing: Tokens.anim.expressiveDefaultSpatial
property: "y"
}
}
remove: Transition {
Anim {
property: "opacity"
to: 0
}
Anim {
property: "scale"
to: 0.6
} }
} }
} }
+3 -4
View File
@@ -16,7 +16,7 @@ ColumnLayout {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
animate: true animate: true
color: Colors.palette.m3onSurfaceVariant color: Colors.palette.m3onSurfaceVariant
font: Tokens.font.body.large font: Tokens.font.title.large
text: Weather.description text: Weather.description
} }
@@ -29,7 +29,7 @@ ColumnLayout {
animate: true animate: true
color: Colors.palette.m3primary color: Colors.palette.m3primary
font: Tokens.font.title.large font: Tokens.font.body.large
text: Weather.temp text: Weather.temp
} }
@@ -37,7 +37,6 @@ ColumnLayout {
animate: true animate: true
color: Colors.palette.m3secondary color: Colors.palette.m3secondary
text: Weather.icon text: Weather.icon
fontStyle: Tokens.font.icon.large
} }
} }
@@ -54,7 +53,7 @@ ColumnLayout {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
animate: true animate: true
color: Colors.palette.m3onSurfaceVariant color: Colors.palette.m3onSurfaceVariant
font: Tokens.font.body.medium font: Tokens.font.body.normal
text: { text: {
const today = Weather.forecast[0]; const today = Weather.forecast[0];
return qsTr("High %1 • Low %2").arg(Weather.formatTemp(today?.maxTempC)).arg(Weather.formatTemp(today?.minTempC)); return qsTr("High %1 • Low %2").arg(Weather.formatTemp(today?.maxTempC)).arg(Weather.formatTemp(today?.minTempC));
+5 -12
View File
@@ -36,7 +36,7 @@ CustomRect {
} }
} }
CustomListView { VerticalFadeListView {
id: layout id: layout
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
@@ -45,20 +45,13 @@ CustomRect {
anchors.margins: Tokens.padding.large anchors.margins: Tokens.padding.large
anchors.right: parent.right anchors.right: parent.right
implicitHeight: contentItem.childrenRect.height implicitHeight: contentItem.childrenRect.height
currentIndex: Weather.currentHourIndex model: Weather.hourlyForecast
// highlightFollowsCurrentItem: true
// preferredHighlightEnd: currentItem.implicitWidth
// preferredHighlightBegin: 0
// highlightRangeMode: ListView.ApplyRange
clip: true
model: Weather.hourlyForecast.slice(Weather.currentHourIndex - 1)
orientation: VerticalFadeListView.Horizontal orientation: VerticalFadeListView.Horizontal
spacing: Tokens.spacing.medium spacing: Tokens.spacing.medium
delegate: ColumnLayout { delegate: ColumnLayout {
id: hour id: hour
readonly property bool current: ListView.view.currentItem === this
readonly property var cond: modelData readonly property var cond: modelData
required property int index required property int index
required property var modelData required property var modelData
@@ -67,7 +60,7 @@ CustomRect {
MaterialShape { MaterialShape {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
color: Qt.alpha(Colors.palette.m3primary, hour.current ? 1 : 0) color: Qt.alpha(Colors.palette.m3primary, hour.index === Weather.currentHourIndex ? 1 : 0)
implicitSize: temp.implicitHeight + Tokens.padding.small * 2 implicitSize: temp.implicitHeight + Tokens.padding.small * 2
shape: MaterialShape.Cookie4Sided shape: MaterialShape.Cookie4Sided
@@ -79,7 +72,7 @@ CustomRect {
id: temp id: temp
anchors.centerIn: parent anchors.centerIn: parent
color: hour.current ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface color: hour.index === Weather.currentHourIndex ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
font: Tokens.font.body.medium font: Tokens.font.body.medium
text: Weather.formatTemp(hour.cond.tempC).slice(0, -1) // Remove C/F text: Weather.formatTemp(hour.cond.tempC).slice(0, -1) // Remove C/F
} }
@@ -103,7 +96,7 @@ CustomRect {
Layout.topMargin: Tokens.spacing.extraSmall Layout.topMargin: Tokens.spacing.extraSmall
color: Colors.palette.m3onSurfaceVariant color: Colors.palette.m3onSurfaceVariant
font: Tokens.font.body.small font: Tokens.font.body.small
text: hour.current ? qsTr("Now") : Qt.formatDateTime(new Date(hour.cond.timestamp.replace("T", " ")), Config.services.useTwelveHourClock ? "ha" : "hh:00") text: hour.index === 0 ? qsTr("Now") : Qt.formatDateTime(new Date(hour.cond.timestamp.replace("T", " ")), Config.services.useTwelveHourClock ? "ha" : "hh:00")
} }
} }
} }
+2 -2
View File
@@ -19,7 +19,6 @@ CustomRect {
return base + brief.anchors.topMargin; return base + brief.anchors.topMargin;
} }
radius: Tokens.rounding.small radius: Tokens.rounding.small
topLeftRadius: Tokens.rounding.largeIncreased
Timer { Timer {
interval: 900000 // 15 minutes interval: 900000 // 15 minutes
@@ -49,6 +48,7 @@ CustomRect {
anchors.right: parent.right anchors.right: parent.right
asynchronous: true asynchronous: true
sourceComponent: Forecast {} sourceComponent: Forecast {
}
} }
} }
+2 -1
View File
@@ -131,7 +131,8 @@ Item {
} }
} }
Behavior on opacity { Behavior on opacity {
Anim {} Anim {
}
} }
} }
} }
+2 -1
View File
@@ -15,7 +15,8 @@ ClipRect {
radius: Tokens.rounding.largeIncreased radius: Tokens.rounding.largeIncreased
Behavior on animPerc { Behavior on animPerc {
Anim {} Anim {
}
} }
Contents { Contents {
+2 -1
View File
@@ -51,7 +51,8 @@ CustomRect {
value: Memory.percentage value: Memory.percentage
Behavior on clampedVal { Behavior on clampedVal {
Anim {} Anim {
}
} }
ColumnLayout { ColumnLayout {
+12 -6
View File
@@ -81,7 +81,8 @@ Item {
active: Config.dashboard.performance.showStorage active: Config.dashboard.performance.showStorage
sourceComponent: StorageCard {} sourceComponent: StorageCard {
}
} }
WrappedLoader { WrappedLoader {
@@ -89,7 +90,8 @@ Item {
active: Config.dashboard.performance.showMemory active: Config.dashboard.performance.showMemory
sourceComponent: MemoryCard {} sourceComponent: MemoryCard {
}
} }
WrappedLoader { WrappedLoader {
@@ -97,7 +99,8 @@ Item {
active: Config.dashboard.performance.showVram active: Config.dashboard.performance.showVram
sourceComponent: VramCard {} sourceComponent: VramCard {
}
} }
WrappedLoader { WrappedLoader {
@@ -105,7 +108,8 @@ Item {
active: Config.dashboard.performance.showNetwork && !vramCard.active active: Config.dashboard.performance.showNetwork && !vramCard.active
sourceComponent: NetworkCard {} sourceComponent: NetworkCard {
}
} }
} }
@@ -114,7 +118,8 @@ Item {
active: Config.dashboard.performance.showNetwork && vramCard.active active: Config.dashboard.performance.showNetwork && vramCard.active
sourceComponent: NetworkCard {} sourceComponent: NetworkCard {
}
} }
} }
@@ -122,7 +127,8 @@ Item {
Layout.fillWidth: false Layout.fillWidth: false
active: Battery.isLaptop && Config.dashboard.performance.showBattery active: Battery.isLaptop && Config.dashboard.performance.showBattery
sourceComponent: BatteryTank {} sourceComponent: BatteryTank {
}
} }
} }
+2 -1
View File
@@ -22,7 +22,8 @@ CustomRect {
topRightRadius: first ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall topRightRadius: first ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
Behavior on opacity { Behavior on opacity {
Anim {} Anim {
}
} }
CustomRect { CustomRect {
+3 -27
View File
@@ -39,7 +39,8 @@ Item {
opacity: img.status === Image.Ready ? 0 : 1 opacity: img.status === Image.Ready ? 0 : 1
Behavior on opacity { Behavior on opacity {
Anim {} Anim {
}
} }
sourceComponent: CustomRect { sourceComponent: CustomRect {
color: Colors.palette.m3primaryContainer color: Colors.palette.m3primaryContainer
@@ -63,43 +64,18 @@ Item {
anchors.fill: parent anchors.fill: parent
asynchronous: true asynchronous: true
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
opacity: status === Image.Ready ? 1 : 0
retainWhileLoading: true retainWhileLoading: true
sourceSize: { sourceSize: {
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1; const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
return Qt.size(width * dpr, height * dpr); return Qt.size(width * dpr, height * dpr);
} }
opacity: status === Image.Ready ? 1 : 0
Behavior on opacity { Behavior on opacity {
Anim { Anim {
type: Anim.SlowEffects type: Anim.SlowEffects
} }
} }
Loader {
opacity: root.modelData.isVideo && img.status === Image.Ready ? 1 : 0
active: opacity > 0
anchors.centerIn: parent
Behavior on opacity {
Anim {
type: Anim.SlowEffects
}
}
sourceComponent: CustomRect {
implicitWidth: videoIcon.implicitWidth + Tokens.padding.extraLarge * 2
implicitHeight: implicitWidth
radius: Tokens.rounding.full
color: Colors.tPalette.m3surface
MaterialIcon {
id: videoIcon
anchors.centerIn: parent
text: "video_library"
fontStyle: Tokens.font.icon.extraLarge
}
}
}
} }
} }
} }
+8 -42
View File
@@ -13,14 +13,9 @@ import qs.Services
Item { Item {
id: wrapper id: wrapper
readonly property bool isVideo: Wallpapers.currentIsVideo property int buttonRowHeight: Tokens.padding.large * 3
readonly property string currentWallpaper: Wallpapers.current
readonly property var currentScreen: screens.length > selectedScreenIndex ? screens[selectedScreenIndex] : null
readonly property real maxZoom: 5.0
readonly property bool changesMade: { readonly property bool changesMade: {
cropRevision; cropRevision;
Wallpapers.cropVersion;
wrapper.currentWallpaper;
const r = cropRectLoader.item; const r = cropRectLoader.item;
if (!r || !currentScreen || scaledImg.status !== Image.Ready) if (!r || !currentScreen || scaledImg.status !== Image.Ready)
@@ -30,9 +25,9 @@ Item {
return Math.abs(r.x - t.x) > 0.5 || Math.abs(r.y - t.y) > 0.5 || Math.abs(r.zoom - t.zoom) > 0.001; return Math.abs(r.x - t.x) > 0.5 || Math.abs(r.y - t.y) > 0.5 || Math.abs(r.zoom - t.zoom) > 0.001;
} }
property int buttonRowHeight: Tokens.padding.large * 3
property int cropRevision: 0 property int cropRevision: 0
readonly property var currentScreen: screens.length > selectedScreenIndex ? screens[selectedScreenIndex] : null
readonly property real maxZoom: 5.0
property var screens: [] property var screens: []
property int selectedScreenIndex: 0 property int selectedScreenIndex: 0
property bool shouldBeActive: true property bool shouldBeActive: true
@@ -118,9 +113,6 @@ Item {
refreshScreens(); refreshScreens();
Qt.callLater(syncCropToScreen); Qt.callLater(syncCropToScreen);
} }
onCurrentWallpaperChanged: {
Qt.callLater(syncCropToScreen);
}
onSelectedScreenIndexChanged: { onSelectedScreenIndexChanged: {
Qt.callLater(syncCropToScreen); Qt.callLater(syncCropToScreen);
} }
@@ -135,7 +127,7 @@ Item {
anchors.rightMargin: Tokens.padding.extraLarge * 2 anchors.rightMargin: Tokens.padding.extraLarge * 2
anchors.top: parent.top anchors.top: parent.top
active: wrapper.screens.length > 1 && !wrapper.isVideo active: wrapper.screens.length > 1
sourceComponent: ButtonRow { sourceComponent: ButtonRow {
id: screenSelector id: screenSelector
@@ -171,7 +163,6 @@ Item {
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
implicitHeight: 30 implicitHeight: 30
enabled: !wrapper.isVideo
CustomSlider { CustomSlider {
id: zoomSlider id: zoomSlider
@@ -283,7 +274,7 @@ Item {
opacity: status === Image.Ready ? 1 : 0 opacity: status === Image.Ready ? 1 : 0
retainWhileLoading: true retainWhileLoading: true
smooth: true smooth: true
source: Wallpapers.currentThumbnail source: Wallpapers.current
sourceSize.height: { sourceSize.height: {
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1; const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
return imageSlot.height * dpr; return imageSlot.height * dpr;
@@ -294,37 +285,12 @@ Item {
type: Anim.SlowEffects type: Anim.SlowEffects
} }
} }
Loader {
opacity: wrapper.isVideo && scaledImg.status === Image.Ready ? 1 : 0
active: opacity > 0
anchors.centerIn: parent
Behavior on opacity {
Anim {
type: Anim.SlowEffects
}
}
sourceComponent: CustomRect {
implicitWidth: videoIcon.implicitWidth + Tokens.padding.extraLarge * 2
implicitHeight: implicitWidth
radius: Tokens.rounding.full
color: Colors.tPalette.m3surface
MaterialIcon {
id: videoIcon
anchors.centerIn: parent
text: "video_library"
fontStyle: Tokens.font.icon.extraLarge
}
}
}
} }
Loader { Loader {
id: cropRectLoader id: cropRectLoader
active: scaledImg.status === Image.Ready && !wrapper.isVideo active: scaledImg.status === Image.Ready
sourceComponent: Component { sourceComponent: Component {
CustomRect { CustomRect {
@@ -367,6 +333,7 @@ Item {
y = t.y; y = t.y;
} }
// Where the rect should be according to saved data (same rules as a restore).
function savedTarget() { function savedTarget() {
const data = Wallpapers.getCrop(wrapper.currentScreen.name); const data = Wallpapers.getCrop(wrapper.currentScreen.name);
const hasCrop = data && (Math.abs(data.x) > 0.001 || Math.abs(data.y) > 0.001 || Math.abs(data.width - 1.0) > 0.001 || Math.abs(data.height - 1.0) > 0.001); const hasCrop = data && (Math.abs(data.x) > 0.001 || Math.abs(data.y) > 0.001 || Math.abs(data.width - 1.0) > 0.001 || Math.abs(data.height - 1.0) > 0.001);
@@ -469,10 +436,9 @@ Item {
} }
anchors.fill: parent anchors.fill: parent
cursorShape: wrapper.isVideo ? Qt.ArrowCursor : pressed ? Qt.ClosedHandCursor : insideCropRect(mouseX, mouseY) ? Qt.OpenHandCursor : Qt.PointingHandCursor cursorShape: pressed ? Qt.ClosedHandCursor : insideCropRect(mouseX, mouseY) ? Qt.OpenHandCursor : Qt.PointingHandCursor
hoverEnabled: true hoverEnabled: true
preventStealing: true preventStealing: true
enabled: !wrapper.isVideo
onPositionChanged: mouse => { onPositionChanged: mouse => {
if (pressed) { if (pressed) {
+1
View File
@@ -38,6 +38,7 @@ Item {
id: container id: container
anchors.fill: parent anchors.fill: parent
layer.enabled: opacity < 1
objectName: "PageContainer" objectName: "PageContainer"
Component.onCompleted: root.loadPage(root.sState.currentPageIdx) Component.onCompleted: root.loadPage(root.sState.currentPageIdx)
@@ -50,12 +50,11 @@ PageBase {
} }
WallItem { WallItem {
id: wallItem required property FileSystemEntry modelData
required property WallpaperEntry modelData
enabled: modelData enabled: modelData
opacity: modelData ? 1 : 0 opacity: modelData ? 1 : 0
source: String(modelData?.thumbnailPath ?? "") source: String(modelData?.path ?? "")
onClicked: { onClicked: {
Wallpapers.setWallpaper(modelData.path); Wallpapers.setWallpaper(modelData.path);
-48
View File
@@ -13,42 +13,6 @@ import qs.Services
PageBase { PageBase {
id: root id: root
readonly property list<MenuItem> wallTransitions: [
MenuItem {
text: "Bowtie"
},
MenuItem {
text: "Fade"
},
MenuItem {
text: "LinearBlur"
},
MenuItem {
text: "WindowSlices"
},
MenuItem {
text: "WipeRight"
},
MenuItem {
text: "WipeDown"
},
MenuItem {
text: "TilesWave"
},
MenuItem {
text: "SplitSlideInHorizontal"
},
MenuItem {
text: "Hexagonalize"
},
MenuItem {
text: "DoomMelt"
},
MenuItem {
text: "Random"
}
]
title: qsTr("Wallpaper & style") title: qsTr("Wallpaper & style")
ColumnLayout { ColumnLayout {
@@ -109,18 +73,6 @@ PageBase {
onToggled: Config.background.enabled = checked onToggled: Config.background.enabled = checked
} }
SelectRow {
Layout.topMargin: Tokens.spacing.extraSmall / 2 - parent.spacing
active: root.wallTransitions[Config.background.wallTransition]
menuItems: root.wallTransitions
menuOnTop: true
settingAnchor: "style-wallpaper-transition"
subtext: qsTr("Currently selected: %1").arg(active.text)
text: qsTr("Wallpaper transition")
onSelected: item => Config.background.wallTransition = root.wallTransitions.indexOf(item)
}
ToggleRow { ToggleRow {
Layout.topMargin: Tokens.spacing.extraSmall / 2 - parent.spacing Layout.topMargin: Tokens.spacing.extraSmall / 2 - parent.spacing
checked: Colors.transparency.enabled checked: Colors.transparency.enabled
@@ -337,7 +337,7 @@ Item {
} }
} }
Item { ClipRect {
id: expandedTool id: expandedTool
anchors.top: collapsedRow.bottom anchors.top: collapsedRow.bottom
@@ -348,16 +348,8 @@ Item {
anchors.topMargin: Tokens.spacing.medium anchors.topMargin: Tokens.spacing.medium
implicitHeight: fullToolCall.implicitHeight + fullToolCall.anchors.margins * 2 implicitHeight: fullToolCall.implicitHeight + fullToolCall.anchors.margins * 2
clip: true radius: Tokens.rounding.medium
color: Colors.palette.m3surfaceContainerLow
CustomRect {
id: bg
anchors.fill: parent
radius: Tokens.rounding.medium
color: Colors.palette.m3surfaceContainerLow
}
CustomText { CustomText {
id: fullToolCall id: fullToolCall
@@ -59,6 +59,7 @@ Item {
id: container id: container
anchors.fill: parent anchors.fill: parent
layer.enabled: opacity < 1
objectName: "ConversationContainer" objectName: "ConversationContainer"
Component.onCompleted: { Component.onCompleted: {
+3 -11
View File
@@ -125,7 +125,7 @@ Item {
current: root.current current: root.current
} }
Item { ClipRect {
id: expandedRect id: expandedRect
anchors.left: parent.left anchors.left: parent.left
@@ -133,19 +133,11 @@ Item {
anchors.top: layout.bottom anchors.top: layout.bottom
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.topMargin: Tokens.spacing.small anchors.topMargin: Tokens.spacing.small
color: Colors.palette.m3surfaceContainerLow
implicitHeight: expandedContent.contentHeight + expandedContent.anchors.margins * 2 implicitHeight: expandedContent.contentHeight + expandedContent.anchors.margins * 2
opacity: root.expanded ? 1 : 0 opacity: root.expanded ? 1 : 0
radius: Tokens.rounding.medium
visible: opacity > 0 visible: opacity > 0
clip: true
CustomRect {
id: bg
anchors.fill: parent
radius: Tokens.rounding.medium
color: Colors.palette.m3surfaceContainerLow
}
Behavior on opacity { Behavior on opacity {
Anim {} Anim {}
+4 -2
View File
@@ -26,7 +26,8 @@ CustomRect {
state: expanded ? "expanded" : "" state: expanded ? "expanded" : ""
Behavior on implicitHeight { Behavior on implicitHeight {
Anim {} Anim {
}
} }
states: State { states: State {
name: "expanded" name: "expanded"
@@ -115,7 +116,8 @@ CustomRect {
anchors.topMargin: Tokens.spacing.extraSmall anchors.topMargin: Tokens.spacing.extraSmall
shouldBeActive: root.expanded shouldBeActive: root.expanded
sourceComponent: ExpandedBody {} sourceComponent: ExpandedBody {
}
} }
component ExpandedBody: ColumnLayout { component ExpandedBody: ColumnLayout {
+6 -3
View File
@@ -58,15 +58,18 @@ LazyListView {
} }
} }
Behavior on scale { Behavior on scale {
Anim {} Anim {
}
} }
Behavior on x { Behavior on x {
Anim {} Anim {
}
} }
Behavior on y { Behavior on y {
enabled: notif.LazyListView.ready enabled: notif.LazyListView.ready
Anim {} Anim {
}
} }
onPositionChanged: event => { onPositionChanged: event => {
+8 -4
View File
@@ -52,7 +52,8 @@ CustomRect {
radius: Tokens.rounding.large radius: Tokens.rounding.large
Behavior on implicitHeight { Behavior on implicitHeight {
Anim {} Anim {
}
} }
Component.onDestruction: { Component.onDestruction: {
@@ -154,7 +155,8 @@ CustomRect {
spacing: root.expanded ? Math.round(Tokens.spacing.extraSmall) : 0 spacing: root.expanded ? Math.round(Tokens.spacing.extraSmall) : 0
Behavior on spacing { Behavior on spacing {
Anim {} Anim {
}
} }
RowLayout { RowLayout {
@@ -212,10 +214,12 @@ CustomRect {
text: "expand_more" text: "expand_more"
Behavior on Layout.topMargin { Behavior on Layout.topMargin {
Anim {} Anim {
}
} }
Behavior on rotation { Behavior on rotation {
Anim {} Anim {
}
} }
} }
} }
+6 -3
View File
@@ -59,15 +59,18 @@ LazyListView {
} }
} }
Behavior on scale { Behavior on scale {
Anim {} Anim {
}
} }
Behavior on x { Behavior on x {
Anim {} Anim {
}
} }
Behavior on y { Behavior on y {
enabled: notif.LazyListView.ready enabled: notif.LazyListView.ready
Anim {} Anim {
}
} }
Component.onCompleted: modelData?.lock(this) Component.onCompleted: modelData?.lock(this)
+2 -1
View File
@@ -17,7 +17,8 @@ ShapePath {
strokeWidth: -1 strokeWidth: -1
Behavior on fillColor { Behavior on fillColor {
CAnim {} CAnim {
}
} }
PathLine { PathLine {
-8
View File
@@ -20,14 +20,6 @@ pkg_check_modules(WAYLAND_CLIENT REQUIRED IMPORTED_TARGET wayland-client)
pkg_check_modules(EGL REQUIRED IMPORTED_TARGET egl) pkg_check_modules(EGL REQUIRED IMPORTED_TARGET egl)
pkg_check_modules(WAYLAND_EGL REQUIRED IMPORTED_TARGET wayland-egl) pkg_check_modules(WAYLAND_EGL REQUIRED IMPORTED_TARGET wayland-egl)
pkg_check_modules(GL REQUIRED IMPORTED_TARGET gl) pkg_check_modules(GL REQUIRED IMPORTED_TARGET gl)
pkg_check_modules(MPV REQUIRED IMPORTED_TARGET mpv)
pkg_check_modules(AVCODEC REQUIRED IMPORTED_TARGET libavcodec)
pkg_check_modules(AVFORMAT REQUIRED IMPORTED_TARGET libavformat)
pkg_check_modules(AVUTIL REQUIRED IMPORTED_TARGET libavutil)
pkg_check_modules(LIBVA QUIET IMPORTED_TARGET libva)
find_package(CUDAToolkit QUIET)
pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED) pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED)
pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED) pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED)
pkg_check_modules(Aubio IMPORTED_TARGET aubio REQUIRED) pkg_check_modules(Aubio IMPORTED_TARGET aubio REQUIRED)
-1
View File
@@ -37,7 +37,6 @@ list(APPEND CMAKE_AUTOMOC_MACRO_NAMES
add_subdirectory(Settings) add_subdirectory(Settings)
add_subdirectory(Models) add_subdirectory(Models)
add_subdirectory(Internal) add_subdirectory(Internal)
add_subdirectory(Wallpaper)
add_subdirectory(Services) add_subdirectory(Services)
add_subdirectory(Components) add_subdirectory(Components)
add_subdirectory(Blobs) add_subdirectory(Blobs)
+1 -6
View File
@@ -1,9 +1,6 @@
#pragma once #pragma once
#include <qqmlintegration.h>
#include "common.hpp" #include "common.hpp"
#include "enums.hpp" #include <qqmlintegration.h>
namespace ZShell::config { namespace ZShell::config {
@@ -12,8 +9,6 @@ class Background : public settings::ObjectNode {
CONFIG_PROPERTY(bool, enabled, true) CONFIG_PROPERTY(bool, enabled, true)
CONFIG_PROPERTY(int, wallFadeDuration, 300) CONFIG_PROPERTY(int, wallFadeDuration, 300)
CONFIG_ENUM_PROPERTY(
WallpaperTransition, wallTransition, WallpaperTransition::Fade)
}; };
} // namespace ZShell::config } // namespace ZShell::config
-13
View File
@@ -23,19 +23,6 @@ ENUM(NotifsFullscreen, On, Off)
ENUM(TemperatureUnit, Auto, Celsius, Fahrenheit, Kelvin) ENUM(TemperatureUnit, Auto, Celsius, Fahrenheit, Kelvin)
ENUM(DataUnit, Binary, Decimal) ENUM(DataUnit, Binary, Decimal)
ENUM(ClockFormat, Auto, TwelveHour, TwentyFourHour) ENUM(ClockFormat, Auto, TwelveHour, TwentyFourHour)
ENUM(
WallpaperTransition,
Bowtie,
Fade,
LinearBlur,
WindowSlices,
WipeRight,
WipeDown,
TilesWave,
SplitSlideInHorizontal,
Hexagonalize,
DoomMelt,
Random)
#undef ENUM #undef ENUM
+53 -20
View File
@@ -14,10 +14,9 @@
#include <hyprland/src/plugins/PluginAPI.hpp> #include <hyprland/src/plugins/PluginAPI.hpp>
#include <hyprland/src/render/OpenGL.hpp> #include <hyprland/src/render/OpenGL.hpp>
#include <hyprland/src/render/Renderer.hpp> #include <hyprland/src/render/Renderer.hpp>
#include <hyprland/src/render/Context.hpp>
#undef private #undef private
#include <hyprland/src/ipc/s2/S2.hpp> #include <hyprland/src/managers/EventManager.hpp>
extern "C" { extern "C" {
#include <lua.h> #include <lua.h>
@@ -28,7 +27,6 @@ extern "C" {
using ZShell::services::kelvinToGain; using ZShell::services::kelvinToGain;
using Render::CRenderContext;
using Render::IFramebuffer; using Render::IFramebuffer;
using Render::GL::CHyprOpenGLImpl; using Render::GL::CHyprOpenGLImpl;
using Render::GL::g_pHyprOpenGL; using Render::GL::g_pHyprOpenGL;
@@ -151,14 +149,25 @@ static bool gainIsNeutral(const std::array<float, 3>& gain) {
} }
static CFunctionHook* g_pBeginHook = nullptr; static CFunctionHook* g_pBeginHook = nullptr;
using origBegin = bool (*)( using origBegin = void (*)(
CHyprOpenGLImpl*, CHyprOpenGLImpl*,
CRenderContext&,
PHLMONITOR, PHLMONITOR,
const CRegion&, const CRegion&,
SP<IFramebuffer>, SP<IFramebuffer>,
std::optional<CRegion>); std::optional<CRegion>);
static CFunctionHook* g_pSaveMirrorHook = nullptr;
using origSaveMirror = bool (*)(CHyprOpenGLImpl*, const CBox&);
static bool hkSaveBufferForMirror(CHyprOpenGLImpl* thisptr, const CBox& box) {
const bool saved = thisptr->m_applyFinalShader;
thisptr->m_applyFinalShader = false;
const bool result = (*reinterpret_cast<origSaveMirror>(
g_pSaveMirrorHook->m_original))(thisptr, box);
thisptr->m_applyFinalShader = saved;
return result;
}
static void installShader() { static void installShader() {
auto* gl = g_pHyprOpenGL.get(); auto* gl = g_pHyprOpenGL.get();
if (!gl) return; if (!gl) return;
@@ -189,33 +198,34 @@ static void setUniformGain(const std::array<float, 3>& gain) {
shader->setUniformFloat3(SHADER_TINT, gain[0], gain[1], gain[2]); shader->setUniformFloat3(SHADER_TINT, gain[0], gain[1], gain[2]);
} }
static bool hkBegin( static void hkBegin(
CHyprOpenGLImpl* thisptr, CHyprOpenGLImpl* thisptr,
CRenderContext& ctx,
PHLMONITOR mon, PHLMONITOR mon,
const CRegion& damage, const CRegion& damage,
SP<IFramebuffer> fb, SP<IFramebuffer> fb,
std::optional<CRegion> finalDamage) { std::optional<CRegion> finalDamage) {
const bool ok = (*reinterpret_cast<origBegin>( (*reinterpret_cast<origBegin>(
g_pBeginHook->m_original))(thisptr, ctx, mon, damage, fb, finalDamage); g_pBeginHook->m_original))(thisptr, mon, damage, fb, finalDamage);
if (!ok) return false;
if (fb) ctx.m_data.blockScreenShader = true; if (fb) {
auto* renderer = g_pHyprRenderer.get();
if (renderer) renderer->m_renderData.blockScreenShader = true;
}
if (!g_active) return true; if (!g_active) return;
auto* gl = g_pHyprOpenGL.get(); auto* gl = g_pHyprOpenGL.get();
if (!gl) return true; if (!gl) return;
if (gl->m_finalScreenShader.get() && gl->m_finalScreenShader->program() < 1) if (gl->m_finalScreenShader.get() && gl->m_finalScreenShader->program() < 1)
installShader(); installShader();
if (!gl->m_finalScreenShader.get() || if (!gl->m_finalScreenShader.get() ||
gl->m_finalScreenShader->program() < 1) gl->m_finalScreenShader->program() < 1)
return true; return;
if (g_transition.active) { if (g_transition.active) {
auto* monPtr = mon.get(); auto* monPtr = mon.get();
if (!monPtr) return true; if (!monPtr) return;
const float elapsed = const float elapsed =
std::chrono::duration<float>( std::chrono::duration<float>(
std::chrono::steady_clock::now() - g_transition.start) std::chrono::steady_clock::now() - g_transition.start)
@@ -255,7 +265,6 @@ static bool hkBegin(
} else if (g_uniformMode) { } else if (g_uniformMode) {
setUniformGain(g_currentGain); setUniformGain(g_currentGain);
} }
return true;
} }
static constexpr int KELVIN_MIN = 2000; static constexpr int KELVIN_MIN = 2000;
@@ -324,10 +333,9 @@ static int luaNightlightState(lua_State* L) {
} }
static void postStateEvent(bool enabled, bool animating) { static void postStateEvent(bool enabled, bool animating) {
auto& sock = IPC::Socket2::sock(); if (!g_pEventManager) return;
if (!sock) return; g_pEventManager->postEvent(
sock->postEvent( SHyprIPCEvent{
IPC::Socket2::SEvent{
.event = "zshell-nightlight", .event = "zshell-nightlight",
.data = std::format( .data = std::format(
"{},{},{},{}", "{},{},{},{}",
@@ -369,6 +377,31 @@ APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) {
throw std::runtime_error("[zshell-nightlight] version mismatch"); throw std::runtime_error("[zshell-nightlight] version mismatch");
} }
static const auto SAVEMIRROR_METHODS =
HyprlandAPI::findFunctionsByName(PHANDLE, "saveBufferForMirror");
const SFunctionMatch* saveMirrorTarget = findExactMethod(
SAVEMIRROR_METHODS,
manglePrefix("CHyprOpenGLImpl", "saveBufferForMirror"));
if (saveMirrorTarget) {
g_pSaveMirrorHook = HyprlandAPI::createFunctionHook(
PHANDLE,
saveMirrorTarget->address,
reinterpret_cast<void*>(&hkSaveBufferForMirror));
if (g_pSaveMirrorHook && g_pSaveMirrorHook->hook())
dlog(
"saveBufferForMirror hook installed: " +
saveMirrorTarget->signature);
}
if (!g_pSaveMirrorHook)
HyprlandAPI::addNotification(
PHANDLE,
"[zshell-nightlight] WARNING: couldn't hook saveBufferForMirror — "
"captures may still show the nightlight",
CHyprColor{1.f, 0.6f, 0.2f, 1.f},
8000);
static const auto METHODS = static const auto METHODS =
HyprlandAPI::findFunctionsByName(PHANDLE, "begin"); HyprlandAPI::findFunctionsByName(PHANDLE, "begin");
const SFunctionMatch* target = const SFunctionMatch* target =
+13 -2
View File
@@ -1,3 +1,8 @@
wl_proto(wlp-layer-shell wlr-layer-shell-unstable-v1 "${CMAKE_CURRENT_SOURCE_DIR}/protocols")
wl_proto(wlp-viewporter viewporter "${WAYLAND_PROTOCOLS}/stable/viewporter")
wl_proto(wlp-fractional-scale fractional-scale-v1 "${WAYLAND_PROTOCOLS}/staging/fractional-scale")
wl_proto(wlp-xdg-shell xdg-shell "${WAYLAND_PROTOCOLS}/stable/xdg-shell")
qml_module(ZShell-internal qml_module(ZShell-internal
URI ZShell.Internal URI ZShell.Internal
INCLUDE_PREFIX internal INCLUDE_PREFIX internal
@@ -8,6 +13,10 @@ qml_module(ZShell-internal
circularindicatormanager.hpp circularindicatormanager.cpp circularindicatormanager.hpp circularindicatormanager.cpp
circularbuffer.hpp circularbuffer.cpp circularbuffer.hpp circularbuffer.cpp
arcgauge.hpp arcgauge.cpp arcgauge.hpp arcgauge.cpp
wallpaperimage.hpp wallpaperimage.cpp
wallpaperservice.hpp wallpaperservice.cpp
wallpapersurface.hpp wallpapersurface.cpp
wallpaperobject.hpp wallpaperobject.cpp
lidwatcher.hpp lidwatcher.cpp lidwatcher.hpp lidwatcher.cpp
visualizerbars.hpp visualizerbars.cpp visualizerbars.hpp visualizerbars.cpp
linearindicatormanager.hpp linearindicatormanager.cpp linearindicatormanager.hpp linearindicatormanager.cpp
@@ -18,14 +27,16 @@ qml_module(ZShell-internal
Qt::Gui Qt::Gui
Qt::Quick Qt::Quick
Qt::Concurrent Qt::Concurrent
Qt::Sql
Qt::Core Qt::Core
Qt::Network Qt::Network
Qt::DBus Qt::DBus
Qt::CanvasPainter Qt::CanvasPainter
Qt::GuiPrivate Qt::GuiPrivate
wlp-layer-shell
wlp-xdg-shell
wlp-viewporter
wlp-fractional-scale
PkgConfig::EGL PkgConfig::EGL
PkgConfig::WAYLAND_EGL PkgConfig::WAYLAND_EGL
PkgConfig::GL PkgConfig::GL
ZShell-config
) )
@@ -8,7 +8,7 @@
#include <QtConcurrent> #include <QtConcurrent>
#include <QQuickWindow> #include <QQuickWindow>
namespace ZShell::wallpaper { namespace ZShell::internal {
QThreadPool* WallpaperImage::loadPool() { QThreadPool* WallpaperImage::loadPool() {
static QThreadPool* pool = [] { static QThreadPool* pool = [] {
@@ -352,4 +352,4 @@ QSGNode* WallpaperImage::updatePaintNode(
return m_rootNode; return m_rootNode;
} }
} // namespace ZShell::wallpaper } // namespace ZShell::internal
@@ -14,7 +14,7 @@
#include <qtmetamacros.h> #include <qtmetamacros.h>
#include <atomic> #include <atomic>
namespace ZShell::wallpaper { namespace ZShell::internal {
class WallpaperImage : public QQuickItem { class WallpaperImage : public QQuickItem {
Q_OBJECT Q_OBJECT
@@ -144,4 +144,4 @@ class WallpaperImage : public QQuickItem {
QSGImageNode* m_oldImageNode = nullptr; QSGImageNode* m_oldImageNode = nullptr;
}; };
} // namespace ZShell::wallpaper } // namespace ZShell::internal
@@ -1,6 +1,6 @@
#include "wallpaperobject.hpp" #include "wallpaperobject.hpp"
namespace ZShell::wallpaper { namespace ZShell::internal {
WallpaperObject::WallpaperObject(QObject* parent) : QObject(parent) {} WallpaperObject::WallpaperObject(QObject* parent) : QObject(parent) {}
@@ -67,44 +67,8 @@ void WallpaperObject::setZoom(qreal zoom) {
push(); push();
} }
void WallpaperObject::syncCrop(const QVariantMap& crop) {
const qreal x = crop.value("x").toDouble();
const qreal y = crop.value("y").toDouble();
const qreal width = crop.contains("width") ? crop.value("width").toDouble()
: 1.0;
const qreal height =
crop.contains("height") ? crop.value("height").toDouble() : 1.0;
const qreal z = crop.contains("zoom") ? crop.value("zoom").toDouble() : 1.0;
const bool prev = m_pushEnabled;
m_pushEnabled = false;
if (!qFuzzyCompare(m_cropX, x)) {
m_cropX = x;
emit cropXChanged();
}
if (!qFuzzyCompare(m_cropY, y)) {
m_cropY = y;
emit cropYChanged();
}
if (!qFuzzyCompare(m_cropWidth, width)) {
m_cropWidth = width;
emit cropWidthChanged();
}
if (!qFuzzyCompare(m_cropHeight, height)) {
m_cropHeight = height;
emit cropHeightChanged();
}
if (!qFuzzyCompare(m_zoom, z)) {
m_zoom = z;
emit zoomChanged();
}
m_pushEnabled = prev;
}
void WallpaperObject::push() { void WallpaperObject::push() {
if (!m_pushEnabled || !m_service || m_screen.isEmpty()) return; if (!m_service || m_screen.isEmpty()) return;
QVariantMap crop; QVariantMap crop;
crop.insert("x", m_cropX); crop.insert("x", m_cropX);
@@ -116,4 +80,4 @@ void WallpaperObject::push() {
m_service->setCrop(m_screen, crop); m_service->setCrop(m_screen, crop);
} }
} // namespace ZShell::wallpaper } // namespace ZShell::internal
@@ -9,7 +9,7 @@
#include "wallpaperservice.hpp" #include "wallpaperservice.hpp"
namespace ZShell::wallpaper { namespace ZShell::internal {
class WallpaperObject : public QObject { class WallpaperObject : public QObject {
Q_OBJECT Q_OBJECT
@@ -55,8 +55,6 @@ class WallpaperObject : public QObject {
qreal zoom() const { return m_zoom; } qreal zoom() const { return m_zoom; }
void setZoom(qreal zoom); void setZoom(qreal zoom);
Q_INVOKABLE void syncCrop(const QVariantMap& crop);
signals: signals:
void serviceChanged(); void serviceChanged();
void screenChanged(); void screenChanged();
@@ -79,8 +77,6 @@ class WallpaperObject : public QObject {
qreal m_cropWidth = 1.0; qreal m_cropWidth = 1.0;
qreal m_cropHeight = 1.0; qreal m_cropHeight = 1.0;
qreal m_zoom = 1.0; qreal m_zoom = 1.0;
bool m_pushEnabled = true;
}; };
} // namespace ZShell::wallpaper } // namespace ZShell::internal
@@ -0,0 +1,756 @@
#define EGL_EGLEXT_PROTOTYPES
#define GL_GLEXT_PROTOTYPES
#include "wallpaperservice.hpp"
#include <EGL/eglext.h>
#include <GL/gl.h>
#include <GL/glext.h>
#include <fcntl.h>
#include <poll.h>
#include <unistd.h>
#include <QDebug>
#include <QGuiApplication>
#include <QImageReader>
#include <QScreen>
#include <qguiapplication_platform.h>
#include <private/qwaylandscreen_p.h>
#include <cstring>
namespace ZShell::internal {
namespace {
const char* kVertexShader = R"(
#version 300 es
void main() {
// The corners are (-1,-1), (3,-1) and (-1,3), which cover the whole buffer.
vec2 corner = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
gl_Position = vec4(corner * 2.0 - 1.0, 0.0, 1.0);
}
)";
const char* kFragmentShader = R"(
#version 300 es
precision highp float;
uniform sampler2D source;
uniform vec2 size;
uniform vec4 crop;
out vec4 outColor;
void main() {
// The pixels are stored top-down, so the fragment coordinate is flipped.
vec2 p = vec2(gl_FragCoord.x, size.y - gl_FragCoord.y) / size;
outColor = texture(source, crop.xy + p * crop.zw);
}
)";
constexpr int kWaitTimeoutMs = 20;
void logShader(uint32_t object, bool program) {
int ok = 0;
if (program)
glGetProgramiv(object, GL_LINK_STATUS, &ok);
else
glGetShaderiv(object, GL_COMPILE_STATUS, &ok);
if (ok) return;
char info[512] = {};
if (program)
glGetProgramInfoLog(object, int(sizeof(info)), nullptr, info);
else
glGetShaderInfoLog(object, int(sizeof(info)), nullptr, info);
qWarning() << "WallpaperService:" << (program ? "linking" : "compiling")
<< "failed:" << info;
}
QVariantMap cropFor(const QVariantMap& crops, const QString& screen) {
QVariantMap crop = crops.value(screen).toMap();
if (crop.isEmpty()) {
crop.insert("x", 0.0);
crop.insert("y", 0.0);
crop.insert("width", 1.0);
crop.insert("height", 1.0);
crop.insert("zoom", 1.0);
}
return crop;
}
} // namespace
WallpaperService::WallpaperService(QObject* parent) : QObject(parent) {}
WallpaperService::~WallpaperService() {
m_stop = true;
wake();
if (m_thread.joinable()) m_thread.join();
for (auto* surface : m_surfaces)
delete surface;
m_surfaces.clear();
if (m_eglDisplay != EGL_NO_DISPLAY && m_context != EGL_NO_CONTEXT) {
if (eglMakeCurrent(
m_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, m_context)) {
if (m_texture) glDeleteTextures(1, &m_texture);
if (m_program) glDeleteProgram(m_program);
}
eglDestroyContext(m_eglDisplay, m_context);
}
if (m_queue) wl_event_queue_destroy(m_queue);
if (m_wake[0] >= 0) {
close(m_wake[0]);
close(m_wake[1]);
}
if (m_layerShell) m_layerShell->destroy();
if (m_viewporter) m_viewporter->destroy();
if (m_scaleManager) m_scaleManager->destroy();
if (m_registry) wl_registry_destroy(m_registry);
}
QString WallpaperService::path() const {
std::lock_guard<std::recursive_mutex> lock(m_mutex);
return m_path;
}
void WallpaperService::setPath(const QString& path) {
if (m_path == path) return;
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
m_path = path;
m_forceDecode = true;
m_imageChanged = true;
}
emit pathChanged();
start();
queueAll();
}
void WallpaperService::addScreen(const QString& screen) {
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
if (m_screens.contains(screen)) return;
m_screens.append(screen);
}
start();
queueAll();
}
void WallpaperService::removeScreen(const QString& screen) {
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
if (!m_screens.removeOne(screen)) return;
}
queueAll();
}
const QVariantMap& WallpaperService::crops() const {
std::lock_guard<std::recursive_mutex> lock(m_mutex);
return m_crops;
}
void WallpaperService::setCrops(const QVariantMap& crops) {
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
if (m_crops == crops) return;
m_crops = crops;
}
emit cropsChanged();
queueAll();
}
void WallpaperService::setCrop(const QString& screen, const QVariantMap& crop) {
WallpaperSurface* surface = nullptr;
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
if (m_crops.value(screen) == crop) return;
m_crops.insert(screen, crop);
for (auto* candidate : m_surfaces) {
if (candidate->screen() == screen) {
surface = candidate;
break;
}
}
}
emit cropsChanged();
if (surface) queue(surface);
}
void WallpaperService::redraw() {
queueAll();
}
void WallpaperService::start() {
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
if (started || m_path.isEmpty() || m_screens.isEmpty()) return;
started = true;
}
auto* interface =
qApp->nativeInterface<QNativeInterface::QWaylandApplication>();
if (!interface) {
qWarning() << "WallpaperService: this is not a Wayland session";
return;
}
m_display = interface->display();
m_compositor = interface->compositor();
if (!m_display || !m_compositor) return;
m_queue = wl_display_create_queue(m_display);
if (!m_queue) return;
if (pipe2(m_wake, O_CLOEXEC) < 0) {
qWarning() << "WallpaperService: could not make the pipe to wake on";
return;
}
bindGlobals();
if (!setupGl()) return;
m_drawing = true;
m_thread = std::thread([this] { loop(); });
wake();
}
void WallpaperService::wake() {
if (m_wake[1] < 0) return;
char byte = 0;
if (::write(m_wake[1], &byte, 1) < 0)
qWarning() << "WallpaperService: could not wake the drawing thread";
}
void WallpaperService::queue(WallpaperSurface* surface) {
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
m_pending.push_back(surface);
}
wake();
}
void WallpaperService::queueAll() {
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
for (auto* surface : m_surfaces)
m_pending.push_back(surface);
}
wake();
}
void WallpaperService::loop() {
while (!m_stop) {
present();
if (wl_display_prepare_read(m_display) == 0) {
wl_display_flush(m_display);
struct pollfd fds[2] = {
{wl_display_get_fd(m_display), POLLIN, 0},
{m_wake[0], POLLIN, 0},
};
int ready = poll(fds, 2, kWaitTimeoutMs);
if (ready > 0 && fds[0].revents) {
wl_display_read_events(m_display);
continue;
}
if (ready > 0 && fds[1].revents) {
char byte = 0;
::read(m_wake[0], &byte, 1);
}
wl_display_cancel_read(m_display);
continue;
}
struct pollfd fds[1] = {{m_wake[0], POLLIN, 0}};
if (poll(fds, 1, kWaitTimeoutMs) <= 0) continue;
char byte = 0;
::read(m_wake[0], &byte, 1);
}
}
void WallpaperService::present() {
wl_display_dispatch_queue_pending(m_display, m_queue);
addSurfaces();
ensureDecodeSize();
std::vector<WallpaperSurface*> pending;
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
pending.swap(m_pending);
}
for (auto* surface : pending)
drawSurface(surface);
}
void WallpaperService::addSurfaces() {
if (!started || !m_layerShell || !m_viewporter || !m_scaleManager) return;
QList<WallpaperSurface*> gone;
QList<QString> needed;
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
QList<WallpaperSurface*> kept;
for (auto* surface : m_surfaces) {
if (m_screens.contains(surface->screen()))
kept.append(surface);
else
gone.append(surface);
}
m_surfaces = kept;
for (const auto& screen : m_screens) {
bool exists = false;
for (auto* surface : m_surfaces) {
if (surface->screen() == screen) {
exists = true;
break;
}
}
if (!exists) needed.append(screen);
}
}
for (auto* surface : gone)
delete surface;
for (const auto& screen : needed)
addSurface(screen);
}
void WallpaperService::registryGlobal(
void* data,
struct wl_registry* registry,
uint32_t name,
const char* interface,
uint32_t version) {
auto* service = static_cast<WallpaperService*>(data);
void* proxy = nullptr;
if (strcmp(interface, "zwlr_layer_shell_v1") == 0) {
proxy = new QtWayland::zwlr_layer_shell_v1(
registry, int(name), int(version));
} else if (strcmp(interface, "wp_viewporter") == 0) {
proxy = new QtWayland::wp_viewporter(registry, int(name), int(version));
} else if (strcmp(interface, "wp_fractional_scale_manager_v1") == 0) {
proxy = new QtWayland::wp_fractional_scale_manager_v1(
registry, int(name), int(version));
} else {
return;
}
service->storeGlobal(interface, proxy);
service->queueAll();
}
void WallpaperService::storeGlobal(const char* interface, void* proxy) {
std::lock_guard<std::recursive_mutex> lock(m_mutex);
if (strcmp(interface, "zwlr_layer_shell_v1") == 0)
m_layerShell = static_cast<QtWayland::zwlr_layer_shell_v1*>(proxy);
else if (strcmp(interface, "wp_viewporter") == 0)
m_viewporter = static_cast<QtWayland::wp_viewporter*>(proxy);
else if (strcmp(interface, "wp_fractional_scale_manager_v1") == 0)
m_scaleManager =
static_cast<QtWayland::wp_fractional_scale_manager_v1*>(proxy);
}
void WallpaperService::bindGlobals() {
m_registry = wl_display_get_registry(m_display);
static const struct wl_registry_listener listener = {
registryGlobal,
nullptr,
};
wl_registry_add_listener(m_registry, &listener, this);
}
bool WallpaperService::setupGl() {
const char* extensions = eglQueryString(EGL_NO_DISPLAY, EGL_EXTENSIONS);
if (extensions && strstr(extensions, "EGL_EXT_platform_wayland")) {
m_eglDisplay =
eglGetPlatformDisplay(EGL_PLATFORM_WAYLAND_EXT, m_display, nullptr);
} else {
m_eglDisplay =
eglGetDisplay(reinterpret_cast<EGLNativeDisplayType>(m_display));
}
if (m_eglDisplay == EGL_NO_DISPLAY) {
qWarning() << "WallpaperService: no EGL display";
return false;
}
EGLint major = 0, minor = 0;
if (!eglInitialize(m_eglDisplay, &major, &minor)) {
qWarning() << "WallpaperService: eglInitialize failed";
return false;
}
const EGLint configAttributes[] = {
EGL_SURFACE_TYPE,
EGL_WINDOW_BIT,
EGL_RENDERABLE_TYPE,
EGL_OPENGL_ES2_BIT,
EGL_RED_SIZE,
8,
EGL_GREEN_SIZE,
8,
EGL_BLUE_SIZE,
8,
EGL_ALPHA_SIZE,
8,
EGL_NONE};
EGLint count = 0;
if (!eglChooseConfig(m_eglDisplay, configAttributes, &m_config, 1, &count) ||
count == 0) {
qWarning() << "WallpaperService: no EGL config for window surfaces";
return false;
}
const EGLint contextAttributes[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_NONE};
m_context = eglCreateContext(
m_eglDisplay, m_config, EGL_NO_CONTEXT, contextAttributes);
if (m_context == EGL_NO_CONTEXT) {
qWarning() << "WallpaperService: eglCreateContext failed";
return false;
}
return true;
}
uint32_t WallpaperService::createProgram() {
uint32_t vertex = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex, 1, &kVertexShader, nullptr);
glCompileShader(vertex);
logShader(vertex, false);
uint32_t fragment = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragment, 1, &kFragmentShader, nullptr);
glCompileShader(fragment);
logShader(fragment, false);
uint32_t program = glCreateProgram();
glAttachShader(program, vertex);
glAttachShader(program, fragment);
glLinkProgram(program);
logShader(program, true);
if (glGetUniformLocation(program, "size") < 0 ||
glGetUniformLocation(program, "crop") < 0) {
qWarning() << "WallpaperService: the program does not have the "
"uniforms it needs";
glDeleteProgram(program);
return 0;
}
m_sizeUniform = glGetUniformLocation(program, "size");
m_cropUniform = glGetUniformLocation(program, "crop");
glDeleteShader(vertex);
glDeleteShader(fragment);
return program;
}
void WallpaperService::uploadImage() {
if (m_image.isNull()) return;
if (m_texture == 0) {
glGenTextures(1, &m_texture);
glBindTexture(GL_TEXTURE_2D, m_texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
} else {
glBindTexture(GL_TEXTURE_2D, m_texture);
}
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexImage2D(
GL_TEXTURE_2D,
0,
GL_RGBA8,
m_image.width(),
m_image.height(),
0,
GL_RGBA,
GL_UNSIGNED_BYTE,
m_image.constBits());
}
bool WallpaperService::decode(const QString& path) {
if (path.isEmpty()) return false;
QImageReader reader(path);
reader.setAutoTransform(true);
QSize size = reader.size();
if (size.isValid()) m_sourceSize = size;
if (!m_decodeSize.isEmpty() && !m_sourceSize.isEmpty())
reader.setScaledSize(
m_sourceSize.scaled(m_decodeSize, Qt::KeepAspectRatioByExpanding));
QImage image = reader.read();
if (image.isNull()) {
qWarning() << "WallpaperService: could not read the image" << path;
return false;
}
m_image = image.convertToFormat(QImage::Format_RGBA8888);
return !m_image.isNull();
}
bool WallpaperService::ensureDecodeSize() {
QSize largest;
QString path;
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
largest = largestBufferSize();
if (largest.isEmpty()) return false;
if (!m_forceDecode && !m_decodeSize.isEmpty() &&
qint64(largest.width()) * largest.height() <=
qint64(m_decodeSize.width()) * m_decodeSize.height())
return false;
QSize target =
m_sourceSize.isEmpty()
? largest
: m_sourceSize.scaled(largest, Qt::KeepAspectRatioByExpanding);
if (!m_forceDecode && !m_image.isNull() && target == m_image.size())
return false;
m_forceDecode = false;
m_decodeSize = largest;
path = m_path;
}
if (!decode(path)) return false;
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
m_imageChanged = true;
}
return true;
}
QSize WallpaperService::largestBufferSize() {
QSize largest;
for (auto* surface : m_surfaces) {
QSize buffer = surface->bufferSize();
if (buffer.isEmpty()) continue;
if (largest.isEmpty() || qint64(buffer.width()) * buffer.height() >
qint64(largest.width()) * largest.height())
largest = buffer;
}
return largest;
}
QRectF WallpaperService::sourceRect(
const QVariantMap& crops, const QString& screen, const QSize& size) const {
QVariantMap crop = cropFor(crops, screen);
QRectF rect(
crop.value("x", 0.0).toDouble(),
crop.value("y", 0.0).toDouble(),
crop.value("width", 1.0).toDouble(),
crop.value("height", 1.0).toDouble());
if (size.isEmpty() || rect.isEmpty() || m_image.isNull()) return rect;
qreal imageWidth = qreal(m_image.width());
qreal imageHeight = qreal(m_image.height());
QRectF pixels(
rect.x() * imageWidth,
rect.y() * imageHeight,
rect.width() * imageWidth,
rect.height() * imageHeight);
qreal targetRatio = qreal(size.width()) / qreal(size.height());
qreal rectRatio = pixels.width() / pixels.height();
if (rectRatio > targetRatio) {
qreal newWidth = pixels.height() * targetRatio;
pixels.setX(pixels.x() + (pixels.width() - newWidth) / 2.0);
pixels.setWidth(newWidth);
} else if (rectRatio < targetRatio) {
qreal newHeight = pixels.width() / targetRatio;
pixels.setY(pixels.y() + (pixels.height() - newHeight) / 2.0);
pixels.setHeight(newHeight);
}
return QRectF(
pixels.x() / imageWidth,
pixels.y() / imageHeight,
pixels.width() / imageWidth,
pixels.height() / imageHeight);
}
void WallpaperService::drawSurface(WallpaperSurface* surface) {
EGLSurface target = surface->eglSurface();
if (m_context == EGL_NO_CONTEXT || target == EGL_NO_SURFACE ||
surface->bufferSize().isEmpty())
return;
if (!eglMakeCurrent(m_eglDisplay, target, target, m_context)) {
qWarning() << "WallpaperService: the context cannot be made current";
return;
}
// Release the context when this scope ends (see FrameService::drawSurface).
// Holding it across the draw loop's sleep serializes the two raw-EGL clients on
// NVIDIA, which makes eglSwapBuffers fail and starves other processes' contexts.
struct ContextRelease {
EGLDisplay display;
~ContextRelease() {
eglMakeCurrent(
display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
}
} contextRelease{m_eglDisplay};
QVariantMap crops;
QSize size;
bool upload = false;
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
crops = m_crops;
size = surface->bufferSize();
upload = m_imageChanged;
m_imageChanged = false;
}
if (size.isEmpty()) return;
if (m_program == 0) m_program = createProgram();
if (m_texture == 0 || upload) uploadImage();
if (m_program == 0 || m_texture == 0 || m_image.isNull()) return;
glUseProgram(m_program);
glBindTexture(GL_TEXTURE_2D, m_texture);
glUniform2f(
GLint(m_sizeUniform), GLfloat(size.width()), GLfloat(size.height()));
QRectF rect = sourceRect(crops, surface->screen(), size);
glUniform4f(
GLint(m_cropUniform),
GLfloat(rect.x()),
GLfloat(rect.y()),
GLfloat(rect.width()),
GLfloat(rect.height()));
glViewport(0, 0, size.width(), size.height());
glDrawArrays(GL_TRIANGLES, 0, 3);
if (!eglSwapBuffers(m_eglDisplay, target))
qWarning()
<< "WallpaperService: eglSwapBuffers failed, eglGetError = 0x"
<< QString::number(unsigned(eglGetError()), 16);
}
void WallpaperService::addSurface(const QString& screen) {
if (!m_layerShell || !m_viewporter || !m_scaleManager) return;
QScreen* qscreen = nullptr;
for (auto* candidate : QGuiApplication::screens()) {
if (candidate->name() == screen) {
qscreen = candidate;
break;
}
}
if (!qscreen) {
qWarning() << "WallpaperService:" << screen << "is not a screen";
return;
}
auto* waylandScreen =
dynamic_cast<QtWaylandClient::QWaylandScreen*>(qscreen->handle());
if (!waylandScreen || !waylandScreen->output()) {
qWarning() << "WallpaperService:" << screen
<< "is not a Wayland screen";
return;
}
wl_surface* wlSurface = wl_compositor_create_surface(m_compositor);
if (!wlSurface) return;
auto* surface =
new WallpaperSurface(this, wlSurface, waylandScreen->output(), screen);
{
std::lock_guard<std::recursive_mutex> lock(m_mutex);
m_surfaces.append(surface);
}
}
} // namespace ZShell::internal
@@ -0,0 +1,147 @@
#pragma once
#include <EGL/egl.h>
#include <cstdint>
#include <mutex>
#include <thread>
#include <vector>
#include <QImage>
#include <QList>
#include <QRectF>
#include <QSize>
#include <QString>
#include <QStringList>
#include <QVariantMap>
#include <QtQml/qqml.h>
#include <qtmetamacros.h>
#include <qwayland-wlr-layer-shell-unstable-v1.h>
#include <wayland-client-protocol.h>
#include "wallpapersurface.hpp"
namespace ZShell::internal {
class WallpaperSurface;
class WallpaperService : public QObject {
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged REQUIRED)
Q_PROPERTY(QVariantMap crops READ crops WRITE setCrops NOTIFY cropsChanged)
public:
explicit WallpaperService(QObject* parent = nullptr);
~WallpaperService() override;
[[nodiscard]] QString path() const;
void setPath(const QString& path);
void addScreen(const QString& screen);
void removeScreen(const QString& screen);
[[nodiscard]] const QVariantMap& crops() const;
void setCrops(const QVariantMap& crops);
Q_INVOKABLE void redraw();
Q_INVOKABLE void setCrop(const QString& screen, const QVariantMap& crop);
signals:
void pathChanged();
void cropsChanged();
private:
friend class WallpaperSurface;
void bindGlobals();
static void registryGlobal(
void* data,
struct wl_registry* registry,
uint32_t name,
const char* interface,
uint32_t version);
void storeGlobal(const char* interface, void* proxy);
[[nodiscard]] bool setupGl();
[[nodiscard]] uint32_t createProgram();
void uploadImage();
bool decode(const QString& path);
bool ensureDecodeSize();
[[nodiscard]] QSize largestBufferSize();
void queue(WallpaperSurface* surface);
void queueAll();
void startDrawing();
void loop();
void present();
void drawSurface(WallpaperSurface* surface);
void wake();
[[nodiscard]] wl_event_queue* eventQueue() const { return m_queue; }
[[nodiscard]] QRectF sourceRect(
const QVariantMap& crops,
const QString& screen,
const QSize& size) const;
void start();
void addSurfaces();
void addSurface(const QString& screen);
QString m_path;
QStringList m_screens;
QVariantMap m_crops;
QImage m_image;
QSize m_sourceSize;
QSize m_decodeSize;
wl_display* m_display = nullptr;
wl_registry* m_registry = nullptr;
wl_compositor* m_compositor = nullptr;
QtWayland::zwlr_layer_shell_v1* m_layerShell = nullptr;
QtWayland::wp_viewporter* m_viewporter = nullptr;
QtWayland::wp_fractional_scale_manager_v1* m_scaleManager = nullptr;
EGLDisplay m_eglDisplay = EGL_NO_DISPLAY;
EGLConfig m_config = nullptr;
EGLContext m_context = EGL_NO_CONTEXT;
uint32_t m_texture = 0;
uint32_t m_program = 0;
int m_sizeUniform = -1;
int m_cropUniform = -1;
QList<WallpaperSurface*> m_surfaces;
std::thread m_thread;
mutable std::recursive_mutex m_mutex;
std::vector<WallpaperSurface*> m_pending;
wl_event_queue* m_queue = nullptr;
int m_wake[2] = {-1, -1};
bool m_stop = false;
bool m_drawing = false;
bool started = false;
bool m_forceDecode = false;
bool m_imageChanged = false;
};
} // namespace ZShell::internal
@@ -6,7 +6,7 @@
#include "wallpaperservice.hpp" #include "wallpaperservice.hpp"
namespace ZShell::wallpaper { namespace ZShell::internal {
const QString kNamespace = "ZShell-Wallpaper"; const QString kNamespace = "ZShell-Wallpaper";
@@ -53,11 +53,6 @@ WallpaperSurface::WallpaperSurface(
} }
WallpaperSurface::~WallpaperSurface() { WallpaperSurface::~WallpaperSurface() {
if (m_frameCallback) {
wl_callback_destroy(m_frameCallback);
m_frameCallback = nullptr;
}
if (m_eglSurface != EGL_NO_SURFACE) if (m_eglSurface != EGL_NO_SURFACE)
eglDestroySurface(m_service->m_eglDisplay, m_eglSurface); eglDestroySurface(m_service->m_eglDisplay, m_eglSurface);
@@ -73,45 +68,8 @@ WallpaperSurface::~WallpaperSurface() {
if (m_surface) wl_surface_destroy(m_surface); if (m_surface) wl_surface_destroy(m_surface);
} }
void WallpaperSurface::markDirty() { void WallpaperSurface::redraw() {
m_dirty.store(true, std::memory_order_relaxed); m_service->queue(this);
}
bool WallpaperSurface::needsDraw() const {
return m_dirty.load(std::memory_order_relaxed);
}
bool WallpaperSurface::frameReady() const {
return m_frameReady.load(std::memory_order_relaxed);
}
void WallpaperSurface::requestFrame() {
if (!m_surface || m_frameCallback) return;
m_frameCallback = wl_surface_frame(m_surface);
static const struct wl_callback_listener listener = {
[](void* data, struct wl_callback*, uint32_t) {
static_cast<WallpaperSurface*>(data)->onFrameDone();
},
};
wl_callback_add_listener(m_frameCallback, &listener, this);
}
void WallpaperSurface::onFrameDone() {
if (m_frameCallback) {
wl_callback_destroy(m_frameCallback);
m_frameCallback = nullptr;
}
m_frameReady.store(true, std::memory_order_relaxed);
m_service->wake();
}
void WallpaperSurface::endDraw() {
m_dirty.store(false, std::memory_order_relaxed);
m_frameReady.store(false, std::memory_order_relaxed);
} }
void WallpaperSurface::zwlr_layer_surface_v1_configure( void WallpaperSurface::zwlr_layer_surface_v1_configure(
@@ -183,9 +141,7 @@ void WallpaperSurface::updateBuffer() {
if (QtWayland::wp_viewport::isInitialized()) if (QtWayland::wp_viewport::isInitialized())
set_destination(m_size.width(), m_size.height()); set_destination(m_size.width(), m_size.height());
requestFrame();
m_service->queue(this); m_service->queue(this);
m_service->notifyBufferSizes();
} }
} // namespace ZShell::wallpaper } // namespace ZShell::internal
@@ -3,8 +3,6 @@
#include <EGL/egl.h> #include <EGL/egl.h>
#include <wayland-egl.h> #include <wayland-egl.h>
#include <atomic>
#include <QSize> #include <QSize>
#include <QString> #include <QString>
@@ -13,7 +11,7 @@
#include <qwayland-wlr-layer-shell-unstable-v1.h> #include <qwayland-wlr-layer-shell-unstable-v1.h>
#include <wayland-client-protocol.h> #include <wayland-client-protocol.h>
namespace ZShell::wallpaper { namespace ZShell::internal {
class WallpaperService; class WallpaperService;
@@ -34,14 +32,7 @@ class WallpaperSurface : public QtWayland::zwlr_layer_surface_v1,
[[nodiscard]] const QSize& bufferSize() const { return m_bufferSize; } [[nodiscard]] const QSize& bufferSize() const { return m_bufferSize; }
[[nodiscard]] EGLSurface eglSurface() const { return m_eglSurface; } [[nodiscard]] EGLSurface eglSurface() const { return m_eglSurface; }
void markDirty(); void redraw();
[[nodiscard]] bool needsDraw() const;
[[nodiscard]] bool frameReady() const;
void requestFrame();
void endDraw();
private: private:
void zwlr_layer_surface_v1_configure( void zwlr_layer_surface_v1_configure(
@@ -51,8 +42,6 @@ class WallpaperSurface : public QtWayland::zwlr_layer_surface_v1,
void updateBuffer(); void updateBuffer();
void onFrameDone();
WallpaperService* m_service; WallpaperService* m_service;
QString m_screen; QString m_screen;
wl_surface* m_surface; wl_surface* m_surface;
@@ -64,10 +53,6 @@ class WallpaperSurface : public QtWayland::zwlr_layer_surface_v1,
qreal m_scale = 1.0; qreal m_scale = 1.0;
bool configured = false; bool configured = false;
std::atomic<bool> m_dirty{false};
std::atomic<bool> m_frameReady{true};
struct wl_callback* m_frameCallback = nullptr;
}; };
} // namespace ZShell::wallpaper } // namespace ZShell::internal
-5
View File
@@ -2,12 +2,7 @@ qml_module(ZShell-models
URI ZShell.Models URI ZShell.Models
SOURCES SOURCES
filesystemmodel.hpp filesystemmodel.cpp filesystemmodel.hpp filesystemmodel.cpp
wallpaperdb.hpp wallpaperdb.cpp
wallpaperutil.hpp
LIBRARIES LIBRARIES
Qt::Gui Qt::Gui
Qt::Quick
Qt::Qml
Qt::Concurrent Qt::Concurrent
Qt::Sql
) )

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