20 Commits
Author SHA1 Message Date
zach 90184bff58 Merge pull request 'feat: video wallpapers' (#163) from feat/video-wallpaper into main
Reviewed-on: #163
2026-10-29 19:55:05 +01:00
zach abdde2450d fix: don't use lambda expression
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 11s
JS/TS / fmt (pull_request) Successful in 7s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Successful in 30s
Rust / build (pull_request) Successful in 50s
Python / verify (pull_request) Failing after 1m27s
Rust / fmt (pull_request) Successful in 31s
Rust / clippy (pull_request) Successful in 58s
2026-10-29 19:50:02 +01:00
zach fd05b3d4c4 Merge branch 'main' into feat/video-wallpaper
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 9s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 13s
JS/TS / lint (pull_request) Successful in 8s
Python / static (pull_request) Failing after 37s
Rust / build (pull_request) Successful in 50s
Python / verify (pull_request) Failing after 1m28s
Rust / fmt (pull_request) Successful in 39s
Rust / clippy (pull_request) Successful in 1m5s
2026-10-29 19:03:10 +01:00
zach 9d8bde1413 fix: update for hyprland 0.57 2026-10-29 11:59:50 +01:00
zach 118b8f482a fix: fix sub-pixel layout arrangement for normalized resolutions
C++ / fmt (pull_request) Failing after 7s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 11s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Failing after 31s
Rust / build (pull_request) Successful in 49s
Python / verify (pull_request) Failing after 1m28s
Rust / fmt (pull_request) Successful in 30s
Rust / clippy (pull_request) Successful in 54s
2026-10-27 23:00:26 +01:00
zach b58b4fdc9c fix: remove more uses of cliprect and finally fix workspace's layer texture
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 9s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Failing after 33s
Rust / build (pull_request) Successful in 48s
Python / verify (pull_request) Failing after 1m24s
Rust / fmt (pull_request) Successful in 32s
Rust / clippy (pull_request) Successful in 57s
2026-10-27 15:15:57 +01:00
zach a56afd7fac fix: don't link eglDestroyImageKHR
C++ / fmt (pull_request) Failing after 7s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 7s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Failing after 27s
Rust / build (pull_request) Successful in 47s
Python / verify (pull_request) Failing after 1m24s
Rust / fmt (pull_request) Successful in 29s
Rust / clippy (pull_request) Successful in 45s
2026-10-27 02:30:55 +01:00
zach b0cb43f75c feat: add vaapi path for video wallpapers on generic gpus
C++ / fmt (pull_request) Failing after 6s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 11s
JS/TS / fmt (pull_request) Successful in 10s
JS/TS / lint (pull_request) Successful in 8s
Python / static (pull_request) Failing after 32s
Rust / build (pull_request) Successful in 52s
Python / verify (pull_request) Failing after 1m23s
Rust / fmt (pull_request) Successful in 51s
Rust / clippy (pull_request) Successful in 1m2s
2026-10-27 02:15:16 +01:00
zach e655ec5bc7 feat: add wallpaper transitions, store crop in database per-wallpaper, move CUDA context creation to async thread
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 9s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 10s
JS/TS / lint (pull_request) Successful in 8s
Python / static (pull_request) Failing after 28s
Python / verify (pull_request) Failing after 1m29s
Rust / build (pull_request) Successful in 53s
Rust / fmt (pull_request) Successful in 42s
Rust / clippy (pull_request) Successful in 1m4s
2026-10-26 14:47:47 +01:00
zach dc0f222ace fix: don't store dbPath as const
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 7s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Failing after 27s
Rust / build (pull_request) Successful in 47s
Python / verify (pull_request) Failing after 1m24s
Rust / fmt (pull_request) Successful in 30s
Rust / clippy (pull_request) Successful in 53s
2026-10-26 03:13:33 +01:00
zach de19fd6ac4 fix: restore code removed by incorrect replacement command
C++ / fmt (pull_request) Failing after 5s
C++ / clang-tidy (pull_request) Failing after 4s
C++ / build (pull_request) Failing after 9s
JS/TS / fmt (pull_request) Successful in 6s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Failing after 28s
Rust / build (pull_request) Successful in 48s
Python / verify (pull_request) Failing after 1m19s
Rust / fmt (pull_request) Successful in 36s
Rust / clippy (pull_request) Successful in 1m0s
2026-10-26 03:03:20 +01:00
zach 9edbcbd610 feat: skip two-way copy for cuda devices when decoding video wallpapers + cache wallpapers as blobs in new database
C++ / fmt (pull_request) Failing after 11s
C++ / build (pull_request) Failing after 14s
C++ / clang-tidy (pull_request) Failing after 9s
JS/TS / fmt (pull_request) Successful in 10s
JS/TS / lint (pull_request) Successful in 8s
Python / static (pull_request) Failing after 28s
Rust / build (pull_request) Successful in 47s
Python / verify (pull_request) Failing after 1m20s
Rust / fmt (pull_request) Successful in 28s
Rust / clippy (pull_request) Successful in 59s
2026-10-26 02:51:28 +01:00
zach cd44d27b74 chore: suppress more warnings
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 7s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Successful in 29s
Rust / build (pull_request) Successful in 59s
Python / verify (pull_request) Failing after 1m31s
Rust / fmt (pull_request) Successful in 33s
Rust / clippy (pull_request) Successful in 52s
2026-10-25 02:10:50 +02:00
zach 0d0faecb1c feat: initial video wallpaper implementation 2026-10-25 02:10:50 +02:00
zach 200a6a5d54 fix: suppress private headers cmake warning 2026-10-25 02:07:38 +02:00
zach 2d2231745c fix: remove layer from settings when fading 2026-10-25 01:49:53 +02:00
zach 187e97a081 fix: notifications no longer hide when dashboard is open 2026-10-24 17:29:17 +02:00
zach 0f54d93675 fix: suppress unnecessary log output 2026-10-24 17:12:35 +02:00
zach 5401cfbc0a feat: implement file and text drop handling for chat and add shrunk sidebar override for window view 2026-10-24 16:28:13 +02:00
zach e35c87b3f2 fix: reduce shader usage and fix greeter 2026-10-24 00:35:24 +02:00
146 changed files with 10073 additions and 2460 deletions
+3
View File
@@ -0,0 +1,3 @@
#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
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install Docker CLI - name: Install Docker CLI
run: apk add --no-cache docker-cli docker-cli-buildx run: apk add --no-cache docker-cli
- name: Login to registry - name: Login to registry
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.aramjonghu.dev --username aramjonghu --password-stdin run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.aramjonghu.dev --username aramjonghu --password-stdin
+19 -1
View File
@@ -20,10 +20,28 @@ jobs:
-exec clang-format -i --style=file {} + -exec clang-format -i --style=file {} +
git diff --exit-code && echo "clang-format: passed" git diff --exit-code && echo "clang-format: passed"
build & lint: build:
runs-on: alpine runs-on: alpine
container: container:
image: git.aramjonghu.dev/aramjonghu/zshell-ci:latest image: git.aramjonghu.dev/aramjonghu/zshell-ci:latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Configure
run: cmake -B build -G Ninja -DENABLE_MODULES=plugin -DCMAKE_BUILD_TYPE=Release
- name: Build
run: ninja -C build
clang-tidy:
runs-on: alpine
container:
image: git.aramjonghu.dev/aramjonghu/zshell-ci:latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
+15 -1
View File
@@ -4,7 +4,7 @@ on:
pull_request: pull_request:
jobs: jobs:
fmt & lint: fmt:
runs-on: alpine runs-on: alpine
container: node:26-alpine container: node:26-alpine
@@ -24,6 +24,20 @@ jobs:
else else
echo "No JS/TS files found" echo "No JS/TS files found"
fi fi
lint:
runs-on: alpine
container: node:26-alpine
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install tools
run: |
apk add --no-cache \
git
- name: ESLint - name: ESLint
run: | run: |
if [ -n "$(find . \( -iname "*.js" -o -iname "*.jsx" -o -iname "*.ts" -o -iname "*.tsx" -o -iname "*.mjs" -o -iname "*.cjs" \) -print -quit)" ]; then if [ -n "$(find . \( -iname "*.js" -o -iname "*.jsx" -o -iname "*.ts" -o -iname "*.tsx" -o -iname "*.mjs" -o -iname "*.cjs" \) -print -quit)" ]; then
+3 -3
View File
@@ -7,7 +7,7 @@ on:
pull_request: pull_request:
jobs: jobs:
fmt & lint: static:
runs-on: debian runs-on: debian
container: node:26-trixie-slim container: node:26-trixie-slim
@@ -33,7 +33,7 @@ jobs:
. .venv/bin/activate . .venv/bin/activate
ruff check . ruff check .
test & nuitka build: verify:
if: always() if: always()
runs-on: debian runs-on: debian
container: node:26-trixie-slim container: node:26-trixie-slim
@@ -64,4 +64,4 @@ jobs:
- name: Nuitka module check - name: Nuitka module check
run: | run: |
. .venv/bin/activate . .venv/bin/activate
nuitka --module --include-package=zshell cli/src/zshell/ nuitka --module --include-package=zshell cli/src/zshell/
+85 -20
View File
@@ -3,9 +3,8 @@ name: Rust
on: on:
pull_request: pull_request:
# jobs can be separated if we integrate tests
jobs: jobs:
build, clippy & fmt: build:
runs-on: alpine runs-on: alpine
container: node:26-alpine container: node:26-alpine
env: env:
@@ -35,9 +34,7 @@ jobs:
apk add --no-cache \ apk add --no-cache \
git \ git \
cargo \ cargo \
rust \ rust
rust-clippy \
rustfmt
- name: Cargo check - name: Cargo check
run: | run: |
@@ -53,22 +50,40 @@ jobs:
else else
echo "No Rust project found" echo "No Rust project found"
fi fi
- name: Clippy
fmt:
runs-on: alpine
container: node:26-alpine
env:
CARGO_HOME: ${{ github.workspace }}/.cargo
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Cache cargo packages
uses: actions/cache@v4
env:
cache-name: cache-cargo-packages
with:
path: |
.cargo/registry
.cargo/git
target
key: rust-fmt-${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
rust-${{ runner.os }}-build-${{ env.cache-name }}-
rust-${{ runner.os }}-build-
rust-
- name: Install tools
run: | run: |
if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then apk add --no-cache \
status=0 git \
for manifest in $(find . -name "Cargo.toml"); do cargo \
cargo clippy --manifest-path "$manifest" --all-targets --all-features -- -D warnings && \ rust \
echo "$manifest: Clippy passed" || \ rustfmt
{ 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: |
if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then
@@ -85,3 +100,53 @@ jobs:
else else
echo "No Rust project found" echo "No Rust project found"
fi fi
clippy:
runs-on: alpine
container: node:26-alpine
env:
CARGO_HOME: ${{ github.workspace }}/.cargo
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Cache cargo packages
uses: actions/cache@v4
env:
cache-name: cache-cargo-packages
with:
path: |
.cargo/registry
.cargo/git
target
key: rust-clippy-${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
rust-${{ runner.os }}-build-${{ env.cache-name }}-
rust-${{ runner.os }}-build-
rust-
- name: Install tools
run: |
apk add --no-cache \
git \
cargo \
rust \
rust-clippy
- 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
+1
View File
@@ -50,6 +50,7 @@ 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")
+3 -2
View File
@@ -1,4 +1,5 @@
import QtQuick import QtQuick
import Quickshell
import ZShell.Components import ZShell.Components
Item { Item {
@@ -19,7 +20,7 @@ Item {
} }
ShaderEffectSource { ShaderEffectSource {
id: source id: src
hideSource: true hideSource: true
sourceItem: contentItem sourceItem: contentItem
@@ -27,7 +28,7 @@ Item {
RoundedMask { RoundedMask {
anchors.fill: parent anchors.fill: parent
source: source source: src
topLeftRadius: root.topLeftRadius topLeftRadius: root.topLeftRadius
topRightRadius: root.topRightRadius topRightRadius: root.topRightRadius
bottomRightRadius: root.bottomRightRadius bottomRightRadius: root.bottomRightRadius
+42 -40
View File
@@ -2,55 +2,57 @@ import QtQuick
import ZShell.Config import ZShell.Config
import qs.Services import qs.Services
CustomClippingRect { Item {
id: track id: track
required property var scrollBar required property var scrollBar
required property var mouseArea required property var mouseArea
anchors.left: parent.left ClipRect {
anchors.right: parent.right
anchors.bottom: parent.bottom
implicitHeight: handle.implicitHeight
radius: Tokens.rounding.full
CustomRect {
id: handle
anchors.bottom: parent.bottom
color: Colors.palette.m3secondary
implicitWidth: track.scrollBar.width * track.scrollBar.effectiveSize
implicitHeight: track.mouseArea.pressed || track.mouseArea.containsMouse ? Tokens.padding.extraSmall * 2 : Tokens.padding.extraSmall
opacity: {
if (!track.scrollBar.enabled)
return 0;
if (track.scrollBar.size === 1)
return 0;
if (track.mouseArea.pressed)
return 1;
if (track.mouseArea.containsMouse)
return 0.8;
if (track.scrollBar.policy === CustomScrollBar.AlwaysOn || track.scrollBar.shouldBeActive)
return 0.6;
return 0;
}
radius: Tokens.rounding.full radius: Tokens.rounding.full
x: track.scrollBar.reversed ? track.scrollBar.width * (1 + track.scrollBar.nonAnimY) * track.scrollBar.travelScale : track.scrollBar.width * track.scrollBar.nonAnimY * track.scrollBar.travelScale anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
implicitHeight: handle.implicitHeight
Behavior on implicitHeight { CustomRect {
Anim {} id: handle
}
Behavior on opacity { anchors.bottom: parent.bottom
Anim { color: Colors.palette.m3secondary
type: Anim.DefaultEffects implicitWidth: track.scrollBar.width * track.scrollBar.effectiveSize
implicitHeight: track.mouseArea.pressed || track.mouseArea.containsMouse ? Tokens.padding.extraSmall * 2 : Tokens.padding.extraSmall
opacity: {
if (!track.scrollBar.enabled)
return 0;
if (track.scrollBar.size === 1)
return 0;
if (track.mouseArea.pressed)
return 1;
if (track.mouseArea.containsMouse)
return 0.8;
if (track.scrollBar.policy === CustomScrollBar.AlwaysOn || track.scrollBar.shouldBeActive)
return 0.6;
return 0;
} }
} radius: Tokens.rounding.full
x: track.scrollBar.reversed ? track.scrollBar.width * (1 + track.scrollBar.nonAnimY) * track.scrollBar.travelScale : track.scrollBar.width * track.scrollBar.nonAnimY * track.scrollBar.travelScale
MouseArea { Behavior on implicitHeight {
acceptedButtons: Qt.NoButton Anim {}
anchors.fill: parent }
cursorShape: Qt.PointingHandCursor Behavior on opacity {
hoverEnabled: true Anim {
type: Anim.DefaultEffects
}
}
MouseArea {
acceptedButtons: Qt.NoButton
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
}
} }
} }
} }
+3 -5
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: opacity < 1 layer.enabled: false
opacity: expanded ? 1 : 0 opacity: expanded ? 1 : 0
parent: { parent: {
const win = QsWindow.window; const win = QsWindow.window;
@@ -101,8 +101,7 @@ MouseArea {
yScale: root.expanded ? 1 : 0.1 yScale: root.expanded ? 1 : 0.1
Behavior on yScale { Behavior on yScale {
Anim { Anim {}
}
} }
} }
@@ -137,8 +136,7 @@ MouseArea {
radius: menu.radius - column.anchors.margins radius: menu.radius - column.anchors.margins
Behavior on radius { Behavior on radius {
Anim { Anim {}
}
} }
StateLayer { StateLayer {
+1 -1
View File
@@ -12,7 +12,7 @@ TextField {
implicitHeight: contentHeight + topPadding + bottomPadding implicitHeight: contentHeight + topPadding + bottomPadding
implicitWidth: contentWidth + leftPadding + rightPadding implicitWidth: contentWidth + leftPadding + rightPadding
placeholderTextColor: Colors.palette.m3onSurfaceVariant // No anim cause placeholder is custom placeholderTextColor: Colors.palette.m3onSurfaceVariant // No anim cause placeholder is custom
renderType: echoMode === TextField.Password ? TextField.QtRendering : TextField.QtRendering renderType: TextField.QtRendering
selectedTextColor: color selectedTextColor: color
selectionColor: Qt.alpha(Colors.palette.m3primary, 0.4) selectionColor: Qt.alpha(Colors.palette.m3primary, 0.4)
verticalAlignment: TextInput.AlignVCenter verticalAlignment: TextInput.AlignVCenter
+1 -1
View File
@@ -30,7 +30,7 @@ CustomFlickable {
type: Anim.SlowEffects type: Anim.SlowEffects
} }
} }
// layer.textureSize: Qt.size(width * dpr, height * dpr) layer.textureSize: Qt.size(width * dpr, height * dpr)
layer.effect: Mask { layer.effect: Mask {
maskSource: mask maskSource: mask
+41 -39
View File
@@ -2,55 +2,57 @@ import QtQuick
import ZShell.Config import ZShell.Config
import qs.Services import qs.Services
CustomClippingRect { Item {
id: track id: track
required property var scrollBar required property var scrollBar
required property var mouseArea required property var mouseArea
anchors.bottom: parent.bottom ClipRect {
anchors.right: parent.right anchors.bottom: parent.bottom
anchors.top: parent.top
implicitWidth: handle.implicitWidth
radius: Tokens.rounding.full
CustomRect {
id: handle
anchors.right: parent.right anchors.right: parent.right
color: Colors.palette.m3secondary anchors.top: parent.top
implicitHeight: track.scrollBar.height * track.scrollBar.effectiveSize
implicitWidth: track.mouseArea.pressed || track.mouseArea.containsMouse ? Tokens.padding.extraSmall * 2 : Tokens.padding.extraSmall
opacity: {
if (!track.scrollBar.enabled)
return 0;
if (track.scrollBar.size === 1)
return 0;
if (track.mouseArea.pressed)
return 1;
if (track.mouseArea.containsMouse)
return 0.8;
if (track.scrollBar.policy === CustomScrollBar.AlwaysOn || track.scrollBar.shouldBeActive)
return 0.6;
return 0;
}
radius: Tokens.rounding.full radius: Tokens.rounding.full
y: track.scrollBar.reversed ? track.scrollBar.height * (1 + track.scrollBar.nonAnimY) * track.scrollBar.travelScale : track.scrollBar.height * track.scrollBar.nonAnimY * track.scrollBar.travelScale implicitWidth: handle.implicitWidth
Behavior on implicitWidth { CustomRect {
Anim {} id: handle
}
Behavior on opacity { anchors.right: parent.right
Anim { color: Colors.palette.m3secondary
type: Anim.DefaultEffects implicitHeight: track.scrollBar.height * track.scrollBar.effectiveSize
implicitWidth: track.mouseArea.pressed || track.mouseArea.containsMouse ? Tokens.padding.extraSmall * 2 : Tokens.padding.extraSmall
opacity: {
if (!track.scrollBar.enabled)
return 0;
if (track.scrollBar.size === 1)
return 0;
if (track.mouseArea.pressed)
return 1;
if (track.mouseArea.containsMouse)
return 0.8;
if (track.scrollBar.policy === CustomScrollBar.AlwaysOn || track.scrollBar.shouldBeActive)
return 0.6;
return 0;
} }
} radius: Tokens.rounding.full
y: track.scrollBar.reversed ? track.scrollBar.height * (1 + track.scrollBar.nonAnimY) * track.scrollBar.travelScale : track.scrollBar.height * track.scrollBar.nonAnimY * track.scrollBar.travelScale
MouseArea { Behavior on implicitWidth {
acceptedButtons: Qt.NoButton Anim {}
anchors.fill: parent }
cursorShape: Qt.PointingHandCursor Behavior on opacity {
hoverEnabled: true Anim {
type: Anim.DefaultEffects
}
}
MouseArea {
acceptedButtons: Qt.NoButton
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
}
} }
} }
} }
+1 -9
View File
@@ -43,13 +43,6 @@ Scope {
hasBar: root.geometry.barOnBottom hasBar: root.geometry.barOnBottom
} }
Timer {
interval: 5000
running: true
onTriggered: console.log("top height:", top.exclusiveZone, "left width:", left.exclusiveZone, "right width:", right.exclusiveZone, "bottom height:", bottom.exclusiveZone)
}
component ExclusionZone: CustomWindow { component ExclusionZone: CustomWindow {
property bool hasBar property bool hasBar
@@ -59,7 +52,6 @@ Scope {
name: "Bar-Exclusion" name: "Bar-Exclusion"
screen: root.screen screen: root.screen
mask: Region { mask: Region {}
}
} }
} }
+7 -6
View File
@@ -19,15 +19,11 @@ import ZShell.Config
Item { Item {
id: root id: root
required property Item bar
required property real borderThickness
readonly property alias clipboard: clipboard readonly property alias clipboard: clipboard
readonly property alias dashboard: dashboard readonly property alias dashboard: dashboard
readonly property alias dashboardWrapper: dashboardWrapper readonly property alias dashboardWrapper: dashboardWrapper
readonly property alias dock: dock readonly property alias dock: dock
readonly property alias drawing: drawing readonly property alias drawing: drawing
required property var drawingItem
required property EdgeGeometry geometry
readonly property alias launcher: launcher readonly property alias launcher: launcher
readonly property alias notifications: notifications readonly property alias notifications: notifications
readonly property alias osd: osd readonly property alias osd: osd
@@ -36,14 +32,19 @@ Item {
readonly property alias popoutsWrapper: popouts readonly property alias popoutsWrapper: popouts
readonly property alias resources: resources readonly property alias resources: resources
readonly property alias resourcesWrapper: resourcesWrapper readonly property alias resourcesWrapper: resourcesWrapper
required property ShellScreen screen
required property ScreenState screenState
readonly property alias settings: settings readonly property alias settings: settings
readonly property alias settingsWrapper: settingsWrapper readonly property alias settingsWrapper: settingsWrapper
readonly property alias sidebar: sidebar readonly property alias sidebar: sidebar
readonly property alias toasts: toasts readonly property alias toasts: toasts
readonly property alias utilities: utilities readonly property alias utilities: utilities
required property Item bar
required property real borderThickness
required property var drawingItem
required property EdgeGeometry geometry
required property ShellScreen screen
required property ScreenState screenState
anchors.bottomMargin: geometry.insetBottom(borderThickness) anchors.bottomMargin: geometry.insetBottom(borderThickness)
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: geometry.insetLeft(borderThickness) anchors.leftMargin: geometry.insetLeft(borderThickness)
+12 -28
View File
@@ -27,27 +27,21 @@ ColumnLayout {
CustomText { CustomText {
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
color: Colors.palette.m3secondary color: Colors.palette.m3secondary
font.bold: true font: Tokens.font.headline.builders.large.scale(3 * root.centerScale).weight(Font.Bold).width(30).build()
font.family: Config.appearance.font.family.clock
font.pointSize: Math.floor(Tokens.font.size.extraLarge * 3 * root.centerScale)
text: Time.hourStr text: Time.hourStr
} }
CustomText { CustomText {
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
color: Colors.palette.m3primary color: Colors.palette.m3primary
font.bold: true font: Tokens.font.headline.builders.large.scale(3 * root.centerScale).weight(Font.Bold).width(30).build()
font.family: Config.appearance.font.family.clock
font.pointSize: Math.floor(Tokens.font.size.extraLarge * 3 * root.centerScale)
text: ":" text: ":"
} }
CustomText { CustomText {
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
color: Colors.palette.m3secondary color: Colors.palette.m3secondary
font.bold: true font: Tokens.font.headline.builders.large.scale(3 * root.centerScale).weight(Font.Bold).width(30).build()
font.family: Config.appearance.font.family.clock
font.pointSize: Math.floor(Tokens.font.size.extraLarge * 3 * root.centerScale)
text: Time.minuteStr text: Time.minuteStr
} }
} }
@@ -56,13 +50,11 @@ ColumnLayout {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Layout.topMargin: -Tokens.padding.large * 2 Layout.topMargin: -Tokens.padding.large * 2
color: Colors.palette.m3tertiary color: Colors.palette.m3tertiary
font.bold: true font: Tokens.font.title.builders.medium.weight(Font.DemiBold).build()
font.family: Config.appearance.font.family.mono
font.pointSize: Math.floor(Tokens.font.size.extraLarge * root.centerScale)
text: Time.format("dddd, d MMMM yyyy") text: Time.format("dddd, d MMMM yyyy")
} }
CustomClippingRect { ClipRect {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Layout.topMargin: Tokens.spacing.largeIncreased * 2 Layout.topMargin: Tokens.spacing.largeIncreased * 2
color: Colors.tPalette.m3surfaceContainer color: Colors.tPalette.m3surfaceContainer
@@ -73,7 +65,7 @@ ColumnLayout {
MaterialIcon { MaterialIcon {
anchors.centerIn: parent anchors.centerIn: parent
color: Colors.palette.m3onSurfaceVariant color: Colors.palette.m3onSurfaceVariant
font.pointSize: Math.floor(root.centerWidth / 4) fontStyle: Tokens.font.icon.size(root.centerWidth / 4).build()
text: "person" text: "person"
} }
@@ -88,9 +80,7 @@ ColumnLayout {
CustomText { CustomText {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
color: Colors.palette.m3onSurfaceVariant color: Colors.palette.m3onSurfaceVariant
font.family: Config.appearance.font.family.mono font: Tokens.font.body.large
font.pointSize: Tokens.font.size.normal
font.weight: 600
text: root.greeter.username text: root.greeter.username
visible: text.length > 0 visible: text.length > 0
} }
@@ -155,8 +145,7 @@ ColumnLayout {
} }
Behavior on opacity { Behavior on opacity {
Anim { Anim {}
}
} }
} }
@@ -191,7 +180,6 @@ ColumnLayout {
anchors.centerIn: parent anchors.centerIn: parent
color: root.greeter.buffer && !root.greeter.launching ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface color: root.greeter.buffer && !root.greeter.launching ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
font.weight: 500
text: root.greeter.launching ? "hourglass_top" : "arrow_forward" text: root.greeter.launching ? "hourglass_top" : "arrow_forward"
} }
} }
@@ -204,8 +192,7 @@ ColumnLayout {
implicitHeight: Math.max(message.implicitHeight, stateMessage.implicitHeight) implicitHeight: Math.max(message.implicitHeight, stateMessage.implicitHeight)
Behavior on implicitHeight { Behavior on implicitHeight {
Anim { Anim {}
}
} }
CustomText { CustomText {
@@ -245,12 +232,10 @@ ColumnLayout {
wrapMode: Text.WrapAtWordBoundaryOrAnywhere wrapMode: Text.WrapAtWordBoundaryOrAnywhere
Behavior on opacity { Behavior on opacity {
Anim { Anim {}
}
} }
Behavior on scale { Behavior on scale {
Anim { Anim {}
}
} }
onMsgChanged: { onMsgChanged: {
@@ -292,8 +277,7 @@ ColumnLayout {
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
color: isError ? Colors.palette.m3error : Colors.palette.m3primary color: isError ? Colors.palette.m3error : Colors.palette.m3primary
font.family: Config.appearance.font.family.mono font: Tokens.font.mono.medium
font.pointSize: Tokens.font.size.small
horizontalAlignment: Qt.AlignHCenter horizontalAlignment: Qt.AlignHCenter
opacity: 0 opacity: 0
scale: 0.7 scale: 0.7
+1 -1
View File
@@ -20,7 +20,7 @@ BusyIndicator {
property int animState property int animState
property color bgColor: Colors.palette.m3secondaryContainer property color bgColor: Colors.palette.m3secondaryContainer
property color fgColor: Colors.palette.m3primary property color fgColor: Colors.palette.m3primary
property real implicitSize: Tokens.font.size.normal * 3 property real implicitSize: Tokens.font.body.medium.pointSize * 3
property real internalStrokeWidth: strokeWidth property real internalStrokeWidth: strokeWidth
readonly property alias progress: manager.progress readonly property alias progress: manager.progress
property real strokeWidth: Tokens.padding.extraSmall * 0.8 property real strokeWidth: Tokens.padding.extraSmall * 0.8
+41
View File
@@ -0,0 +1,41 @@
import QtQuick
import ZShell.Components
Item {
id: root
property real radius
property real topLeftRadius: radius
property real topRightRadius: radius
property real bottomRightRadius: radius
property real bottomLeftRadius: radius
property color color: "transparent"
default property alias contentData: contentItem.data
Item {
id: contentItem
anchors.fill: parent
}
ShaderEffectSource {
id: source
hideSource: true
sourceItem: contentItem
}
RoundedMask {
anchors.fill: parent
source: source
topLeftRadius: root.topLeftRadius
topRightRadius: root.topRightRadius
bottomRightRadius: root.bottomRightRadius
bottomLeftRadius: root.bottomLeftRadius
color: root.color
}
Behavior on color {
CAnim {}
}
}
+23
View File
@@ -1,13 +1,36 @@
import QtQuick import QtQuick
import qs.Services
Flickable { Flickable {
id: root id: root
property bool doneFakeFlick
interactive: !ShellState.forActive().isDrawing
maximumFlickVelocity: 3000 maximumFlickVelocity: 3000
rebound: Transition { rebound: Transition {
onRunningChanged: {
if (!running && !root.doneFakeFlick) {
root.doneFakeFlick = true;
root.flick(1, 1);
root.flick(-1, -1);
Qt.callLater(() => root.cancelFlick());
}
}
Anim { Anim {
properties: "x,y" properties: "x,y"
} }
} }
Timer {
interval: 10
running: root.doneFakeFlick
onTriggered: {
root.doneFakeFlick = false;
root.returnToBounds();
}
}
} }
+23
View File
@@ -1,13 +1,36 @@
import QtQuick import QtQuick
import qs.Services
ListView { ListView {
id: root id: root
property bool doneFakeFlick
interactive: !ShellState.forActive()?.isDrawing
maximumFlickVelocity: 3000 maximumFlickVelocity: 3000
rebound: Transition { rebound: Transition {
// onRunningChanged: {
// if (!running && !root.doneFakeFlick) {
// root.doneFakeFlick = true;
// root.flick(1, 1);
// root.flick(-1, -1);
// Qt.callLater(() => root.cancelFlick());
// }
// }
Anim { Anim {
properties: "x,y" properties: "x,y"
type: Anim.DefaultEffects
} }
} }
// Timer {
// interval: 10
// running: root.doneFakeFlick
//
// onTriggered: {
// root.doneFakeFlick = false;
// }
// }
} }
+1
View File
@@ -4,6 +4,7 @@ MouseArea {
property int scrollAccumulatedY: 0 property int scrollAccumulatedY: 0
function onWheel(event: WheelEvent): void { function onWheel(event: WheelEvent): void {
event.accepted = false;
} }
onWheel: event => { onWheel: event => {
+1 -2
View File
@@ -6,7 +6,6 @@ Rectangle {
color: "transparent" color: "transparent"
Behavior on color { Behavior on color {
CAnim { CAnim {}
}
} }
} }
+9 -7
View File
@@ -14,10 +14,9 @@ Text {
property real animateTo: 1 property real animateTo: 1
color: Colors.palette.m3onSurface color: Colors.palette.m3onSurface
font.family: Config.appearance.font.family.sans font: Tokens.font.body.small
font.pointSize: Tokens.font.size.normal
linkColor: Colors.palette.m3onPrimaryFixedVariant linkColor: Colors.palette.m3onPrimaryFixedVariant
renderType: Text.NativeRendering renderType: Text.QtRendering
textFormat: Text.PlainText textFormat: Text.PlainText
Behavior on color { Behavior on color {
@@ -28,22 +27,25 @@ Text {
enabled: root.animate enabled: root.animate
SequentialAnimation { SequentialAnimation {
Anim { TAnim {
target: root
to: root.animateFrom to: root.animateFrom
type: Anim.FastEffects
} }
PropertyAction { PropertyAction {
} }
Anim { TAnim {
target: root
to: root.animateTo to: root.animateTo
type: Anim.DefaultEffects
} }
} }
} }
component Anim: NumberAnimation { component TAnim: Anim {
duration: root.animateDuration / 2 duration: root.animateDuration / 2
easing.type: Easing.BezierSpline
properties: root.animateProp.split(",").length > 1 ? root.animateProp : "" properties: root.animateProp.split(",").length > 1 ? root.animateProp : ""
property: root.animateProp.split(",").length === 1 ? root.animateProp : "" property: root.animateProp.split(",").length === 1 ? root.animateProp : ""
target: root target: root
+275 -46
View File
@@ -1,76 +1,305 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Shapes
import ZShell.Config import ZShell.Config
import qs.Services import qs.Services
TextField { TextFieldBase {
id: root id: root
background: null enum TextFieldType {
color: Colors.palette.m3onSurface Outlined,
cursorVisible: !readOnly Filled
font.family: Config.appearance.font.family.sans }
font.pointSize: Tokens.font.size.smaller
placeholderTextColor: Colors.palette.m3outline
renderType: echoMode === TextField.Password ? TextField.QtRendering : TextField.NativeRendering
Behavior on color { readonly property int clampedRadius: Math.min(horizontalPadding, Math.min(width, height) / 2, radius)
CAnim { readonly property string effectiveSupportingText: isError && errorText ? errorText : supportingText
readonly property int filledOffset: type === CustomTextField.Filled ? Tokens.spacing.small : 0
readonly property int leadingOffset: leadingIcon ? leadingIconLoader.width + leadingIconLoader.anchors.leftMargin : 0
readonly property real smallFontScale: smallFontSize / font.pointSize
readonly property int supportingTextOffset: effectiveSupportingText ? supportingTextLoader.height + Tokens.spacing.extraSmall : 0
readonly property int trailingOffset: trailingIcon ? trailingIconLoader.width + trailingIconLoader.anchors.rightMargin : 0
readonly property bool valid: !validate || (!text && emptyIsValid) || (validate instanceof RegExp ? validate.test(text) : !!validate(text))
property int horizontalPadding: Tokens.padding.large
property int verticalPadding: Tokens.padding.large
property int radius: Tokens.rounding.small
property string supportingText
property string trailingIcon
property int smallFontSize: Tokens.font.label.small.pointSize
property int type: CustomTextField.Outlined
property var validate // Regex or function
property bool emptyIsValid: true
property string errorText
property bool isError
property string leadingIcon
bottomPadding: verticalPadding + supportingTextOffset - filledOffset
leftPadding: horizontalPadding + leadingOffset
rightPadding: horizontalPadding + trailingOffset
topPadding: verticalPadding + filledOffset
background: Loader {
anchors.bottomMargin: root.supportingTextOffset
anchors.fill: parent
sourceComponent: root.type === CustomTextField.Filled ? filledComp : outlineComp
StateLayer {
id: stateLayer
cursorShape: Qt.IBeamCursor
enabled: !root.activeFocus
manualPressOverride: tapHandler.pressed
radius: root.type === CustomTextField.Outlined ? root.clampedRadius : 0
topLeftRadius: root.clampedRadius
topRightRadius: root.clampedRadius
onClicked: root.focus = true
} }
} }
cursorDelegate: CustomRect {
id: cursor
property bool disableBlink onEditingFinished: {
if (!valid)
isError = true;
}
onPressed: {
if (stateLayer.enabled)
stateLayer.press(stateLayer.mouseX, stateLayer.mouseY);
}
onTextEdited: {
if (isError)
isError = false;
}
color: Colors.palette.m3primary Item {
implicitWidth: 2 id: contentWrapper
radius: Tokens.rounding.medium
Behavior on opacity { anchors.bottomMargin: root.supportingTextOffset
Anim { anchors.fill: parent
duration: Tokens.anim.durations.small
}
}
Connections { CustomText {
function onCursorPositionChanged(): void { id: placeholder
if (root.activeFocus && root.cursorVisible) {
cursor.opacity = 1; anchors.left: parent.left
cursor.disableBlink = true; anchors.leftMargin: root.leftPadding
enableBlink.restart(); anchors.topMargin: Tokens.padding.extraSmall
anchors.verticalCenter: parent.verticalCenter
color: root.isError ? Colors.palette.m3error : (root.activeFocus ? Colors.palette.m3primary : root.text ? Colors.palette.m3outline : root.placeholderTextColor)
font.family: root.font.family
font.pointSize: root.font.pointSize
font.variableAxes: root.font.variableAxes
font.weight: root.font.weight
renderType: Text.QtRendering
text: root.placeholderText
states: [
State {
name: "smallOutlined"
when: root.type === CustomTextField.Outlined && (root.activeFocus || root.text)
PropertyChanges {
placeholder.anchors.leftMargin: -(1 - root.smallFontScale) * placeholder.width / 2 + root.horizontalPadding
placeholder.scale: root.smallFontScale
}
AnchorChanges {
anchors.verticalCenter: contentWrapper.top
target: placeholder
}
},
State {
name: "smallFilled"
when: root.type === CustomTextField.Filled && (root.activeFocus || root.text)
PropertyChanges {
placeholder.anchors.leftMargin: -(1 - root.smallFontScale) * placeholder.width / 2 + root.horizontalPadding + root.leadingOffset
placeholder.scale: root.smallFontScale
}
AnchorChanges {
anchors.top: contentWrapper.top
anchors.verticalCenter: undefined
target: placeholder
}
}
]
transitions: Transition {
Anim {
properties: "scale,leftMargin"
type: Anim.DefaultEffects
}
AnchorAnim {
duration: Tokens.anim.durations.expressiveDefaultEffects
easing: Tokens.anim.expressiveDefaultEffects
} }
} }
target: root
} }
Timer { Loader {
id: enableBlink id: leadingIconLoader
interval: 100 active: root.leadingIcon
anchors.left: parent.left
anchors.leftMargin: Tokens.padding.small
anchors.verticalCenter: parent.verticalCenter
onTriggered: cursor.disableBlink = false sourceComponent: MaterialIcon {
color: Colors.palette.m3onSurfaceVariant
text: root.leadingIcon
fontStyle: Tokens.font.icon.builders.medium.scale(0.9).build()
}
} }
Timer { Loader {
interval: 500 id: trailingIconLoader
repeat: true
running: root.activeFocus && root.cursorVisible && !cursor.disableBlink
triggeredOnStart: true
onTriggered: parent.opacity = parent.opacity === 1 ? 0 : 1 active: root.trailingIcon
} anchors.right: parent.right
anchors.rightMargin: Tokens.padding.small
anchors.verticalCenter: parent.verticalCenter
Binding { sourceComponent: MaterialIcon {
cursor.opacity: 0 color: Colors.palette.m3onSurfaceVariant
when: !root.activeFocus || !root.cursorVisible text: root.trailingIcon
fontStyle: Tokens.font.icon.builders.medium.scale(0.9).build()
}
} }
} }
Behavior on placeholderTextColor {
CAnim { Loader {
id: supportingTextLoader
active: root.effectiveSupportingText
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.leftMargin: root.horizontalPadding
sourceComponent: CustomText {
color: root.isError ? Colors.palette.m3error : Colors.palette.m3onSurfaceVariant
font: Tokens.font.label.small
text: root.effectiveSupportingText
}
}
TapHandler {
id: tapHandler
}
Component {
id: outlineComp
Shape {
id: bg
asynchronous: true
preferredRendererType: Shape.CurveRenderer
ShapePath {
id: path
readonly property real inset: strokeWidth / 2
readonly property real outlineGap: placeholder.width * root.smallFontScale + Tokens.spacing.small * 2
property real outlineGapScale: root.activeFocus || root.text ? 1 : 0
capStyle: ShapePath.RoundCap
fillColor: "transparent"
startX: path.inset + root.horizontalPadding - root.clampedRadius + path.outlineGap * (1 - path.outlineGapScale) / 2 + path.outlineGap * path.outlineGapScale
startY: path.inset
strokeColor: root.isError ? Colors.palette.m3error : (root.activeFocus ? Colors.palette.m3primary : Colors.palette.m3outline)
strokeWidth: root.activeFocus ? 2 : 1
Behavior on outlineGapScale {
Anim {
type: Anim.DefaultEffects
}
}
Behavior on strokeColor {
CAnim {}
}
Behavior on strokeWidth {
Anim {}
}
PathLine {
x: bg.width - path.inset - root.clampedRadius
y: path.inset
}
PathArc {
radiusX: root.clampedRadius
radiusY: root.clampedRadius
x: bg.width - path.inset
y: path.inset + root.clampedRadius
}
PathLine {
x: bg.width - path.inset
y: bg.height - path.inset - root.clampedRadius
}
PathArc {
radiusX: root.clampedRadius
radiusY: root.clampedRadius
x: bg.width - path.inset - root.clampedRadius
y: bg.height - path.inset
}
PathLine {
x: path.inset + root.clampedRadius
y: bg.height - path.inset
}
PathArc {
radiusX: root.clampedRadius
radiusY: root.clampedRadius
x: path.inset
y: bg.height - path.inset - root.clampedRadius
}
PathLine {
x: path.inset
y: path.inset + root.clampedRadius
}
PathArc {
radiusX: root.clampedRadius
radiusY: root.clampedRadius
x: path.inset + root.clampedRadius
y: path.inset
}
PathLine {
x: path.inset + root.horizontalPadding - root.clampedRadius / 2 + path.outlineGap * (1 - path.outlineGapScale) / 2
y: path.inset
}
}
}
}
Component {
id: filledComp
CustomRect {
color: root.activeFocus ? Colors.tPalette.m3surfaceContainerHighest : Colors.tPalette.m3surfaceContainerHigh
topLeftRadius: root.clampedRadius
topRightRadius: root.clampedRadius
CustomRect {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
color: root.isError ? Colors.palette.m3error : (root.activeFocus ? Colors.palette.m3primary : Colors.palette.m3outline)
implicitHeight: root.activeFocus ? 2 : 1
Behavior on implicitHeight {
Anim {}
}
}
} }
} }
} }
+3 -2
View File
@@ -1,15 +1,16 @@
import QtQuick import QtQuick
import QtQuick.Controls
import ZShell.Config import ZShell.Config
import qs.Services import qs.Services
TextInput { TextInput {
renderType: Text.NativeRendering renderType: Text.NativeRendering
selectedTextColor: Colors.palette.m3onSecondaryContainer selectedTextColor: Colors.palette.m3onSecondaryContainer
selectionColor: Colors.tPalette.m3onSecondaryContainer selectionColor: Colors.tPalette.colSecondaryContainer
font { font {
family: Config.appearance?.font.family.sans ?? "sans-serif" family: Config.appearance?.font.family.sans ?? "sans-serif"
hintingPreference: Font.PreferFullHinting hintingPreference: Font.PreferFullHinting
pixelSize: Tokens?.font.size.normal ?? 15 pixelSize: Config.appearance?.font.size.normal ?? 15
} }
} }
+79
View File
@@ -0,0 +1,79 @@
import QtQuick
import Quickshell
import ZShell.Config
Image {
id: root
property int fadeInAnim: Anim.DefaultEffects
property int fadeInLargeAnim: Anim.StandardLarge
property int fadeOutAnim: Anim.FastEffects
property bool fadingOut
property bool hadPrevious
property bool preventInit
function maybeStartInAnim(): void {
if (!preventInit && !opacityInAnim.running && status === Image.Ready) {
opacityInAnim.type = hadPrevious ? fadeInAnim : fadeInLargeAnim;
opacityInAnim.start();
}
}
asynchronous: true
fillMode: Image.PreserveAspectCrop
opacity: 0
retainWhileLoading: true
sourceSize: {
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
return Qt.size(width * dpr, height * dpr);
}
Anim on opacity {
id: opacityInAnim
running: false
to: 1
}
Behavior on source {
SequentialAnimation {
ScriptAction {
script: opacityInAnim.stop()
}
PropertyAction {
property: "fadingOut"
target: root
value: true
}
Anim {
property: "opacity"
target: root
to: 0
type: root.fadeOutAnim
}
PropertyAction {
property: "fadingOut"
target: root
value: false
}
PropertyAction {
property: "hadPrevious"
target: root
value: root.source
}
PropertyAction {
}
ScriptAction {
script: root.maybeStartInAnim()
}
}
}
onPreventInitChanged: maybeStartInAnim()
onStatusChanged: maybeStartInAnim()
}
+115
View File
@@ -0,0 +1,115 @@
import QtQuick
import Quickshell
import M3Shapes
import ZShell.Config
import qs.Services
MaterialShape {
id: root
property bool animated: true
property real cRotation
property bool containsIcon
property real dampingRatio: 0.6
property real lRotation
property int morphAnimRotation: 60
property real morphScale: 0.14
property alias rotateAnimDuration: rotateAnim.duration
property int shapeIndex
property list<int> shapes: {
if (containsIcon)
return [MaterialShape.SoftBurst, MaterialShape.Cookie9Sided, MaterialShape.Pill, MaterialShape.Sunny, MaterialShape.Cookie4Sided, MaterialShape.Oval];
return [MaterialShape.SoftBurst, MaterialShape.Cookie9Sided, MaterialShape.Pentagon, MaterialShape.Pill, MaterialShape.Sunny, MaterialShape.Cookie4Sided, MaterialShape.Oval];
}
readonly property real springDuration: {
const wn = Math.sqrt(stiffness);
const r = -dampingRatio * wn;
const c = 1 / Math.sqrt(1 - dampingRatio * dampingRatio);
return Math.log(visibilityThreshold / c) / r;
}
readonly property real springMaxVelocity: {
const wn = Math.sqrt(stiffness);
const factor = Math.exp(-z * Math.acos(z) / Math.sqrt(1 - z * z));
return wn * factor;
}
property bool springSettled: true
property real stiffness: 180
property real thisLRotation
property real visibilityThreshold: 0.075
function spring(t: real): var {
const wn = Math.sqrt(stiffness);
const za = dampingRatio * wn;
const wd = wn * Math.sqrt(1 - dampingRatio * dampingRatio);
const r = za / wd;
const pos = 1 - Math.exp(-za * t) * (Math.cos(wd * t) + r * Math.sin(wd * t));
const vel = Math.exp(-za * t) * (wn * wn / wd) * Math.sin(wd * t);
return [pos, vel];
}
color: Colors.palette.m3primary
implicitSize: 38
toShape: shapes[0]
RotationAnimation on cRotation {
id: rotateAnim
duration: 4666
easing.type: Easing.Linear
from: 0
loops: Animation.Infinite
running: root.animated
to: 360
}
Behavior on color {
CAnim {
}
}
ElapsedTimer {
id: timer
}
FrameAnimation {
running: root.animated && !root.springSettled
onTriggered: {
const t = timer.elapsed();
if (t >= root.springDuration) {
root.springSettled = true;
} else {
const [pos, vel] = root.spring(t);
root.morphProgress = Math.min(1, pos); // Overshooting the morph looks weird
root.thisLRotation = pos * root.morphAnimRotation;
root.scale = 1 + vel * root.morphScale / root.springMaxVelocity;
}
}
}
Timer {
interval: 650
repeat: true
running: root.animated
triggeredOnStart: true
onTriggered: {
root.beginBatchUpdate();
root.fromShape = root.toShape;
root.shapeIndex = (root.shapeIndex + 1) % root.shapes.length;
root.toShape = root.shapes[root.shapeIndex];
root.morphProgress = 0;
root.rotation = root.rotation;
root.lRotation = (root.lRotation + root.thisLRotation) % 360;
root.thisLRotation = 0;
root.rotation = Qt.binding(() => root.cRotation + root.lRotation + root.thisLRotation);
root.springSettled = false;
timer.restart();
root.endBatchUpdate();
}
}
}
+8 -10
View File
@@ -1,16 +1,14 @@
import QtQuick
import ZShell.Config import ZShell.Config
import qs.Services import qs.Services
CustomText { CustomText {
property real fill id: root
property int grade: Colors.light ? 0 : -25
font.family: "Material Symbols Rounded" property real fill: 0
font.pointSize: Tokens.font.size.larger property int grade: Colors.light ? 0 : -25
font.variableAxes: ({ property font fontStyle: Tokens.font.icon.small
FILL: fill.toFixed(1),
GRAD: grade, renderType: Text.NativeRendering
opsz: fontInfo.pixelSize, font: Tokens.font.icon.size(fontStyle.pointSize).weight(fontStyle.weight).vaxes(fontStyle.variableAxes).fill(fill.toFixed(1)).grade(grade).build()
wght: fontInfo.weight
})
} }
+6 -8
View File
@@ -1,8 +1,6 @@
import QtQuick import QtQuick
import QtQuick.Shapes import QtQuick.Shapes
import ZShell import ZShell
import ZShell.Components
import qs.Helpers
import ZShell.Config import ZShell.Config
import qs.Services import qs.Services
@@ -13,7 +11,6 @@ MouseArea {
property alias bottomRightRadius: base.bottomRightRadius property alias bottomRightRadius: base.bottomRightRadius
property real circleRadius property real circleRadius
property alias color: base.color property alias color: base.color
property bool disabled
readonly property real endRadius: { readonly property real endRadius: {
const d1 = distSq(0, 0); const d1 = distSq(0, 0);
const d2 = distSq(width, 0); const d2 = distSq(width, 0);
@@ -22,6 +19,7 @@ MouseArea {
return (Math.sqrt(Math.max(d1, d2, d3, d4)) + (shapeMorph ? 24 : 0)) * 1.3; return (Math.sqrt(Math.max(d1, d2, d3, d4)) + (shapeMorph ? 24 : 0)) * 1.3;
} }
property real endRadiusAtPress property real endRadiusAtPress
property bool manualHoverOverride
property bool manualPressOverride property bool manualPressOverride
property real pressX: width / 2 property real pressX: width / 2
property real pressY: height / 2 property real pressY: height / 2
@@ -29,7 +27,7 @@ MouseArea {
readonly property alias rect: base readonly property alias rect: base
property bool shapeMorph property bool shapeMorph
property bool showHoverBackground: true property bool showHoverBackground: true
property real stateOpacity: containsMouse ? 0.08 : 0 property real stateOpacity: containsMouse || manualHoverOverride ? 0.08 : 0
property alias topLeftRadius: base.topLeftRadius property alias topLeftRadius: base.topLeftRadius
property alias topRightRadius: base.topRightRadius property alias topRightRadius: base.topRightRadius
@@ -52,8 +50,8 @@ MouseArea {
} }
anchors.fill: parent anchors.fill: parent
cursorShape: disabled ? undefined : Qt.PointingHandCursor cursorShape: !enabled ? undefined : Qt.PointingHandCursor
enabled: !disabled enabled: parent.enabled
hoverEnabled: true hoverEnabled: true
Behavior on stateOpacity { Behavior on stateOpacity {
@@ -100,13 +98,13 @@ MouseArea {
id: base id: base
anchors.fill: parent anchors.fill: parent
bottomLeftRadius: root.parent?.bottomLeftRadius ?? radius ?? 0
bottomRightRadius: root.parent?.bottomRightRadius ?? radius ?? 0
color: Colors.palette.m3onSurface color: Colors.palette.m3onSurface
opacity: root.stateOpacity opacity: root.stateOpacity
// Pick up radius from parent if it has one (parent can be anything with radius props) // Pick up radius from parent if it has one (parent can be anything with radius props)
// qmllint disable missing-property // qmllint disable missing-property
radius: root.parent?.radius ?? 0 radius: root.parent?.radius ?? 0
bottomLeftRadius: root.parent?.bottomLeftRadius ?? radius ?? 0
bottomRightRadius: root.parent?.bottomRightRadius ?? radius ?? 0
topLeftRadius: root.parent?.topLeftRadius ?? radius ?? 0 topLeftRadius: root.parent?.topLeftRadius ?? radius ?? 0
topRightRadius: root.parent?.topRightRadius ?? radius ?? 0 topRightRadius: root.parent?.topRightRadius ?? radius ?? 0
// qmllint enable missing-property // qmllint enable missing-property
+16 -39
View File
@@ -13,34 +13,16 @@ RowLayout {
spacing: Tokens.spacing.largeIncreased * 2 spacing: Tokens.spacing.largeIncreased * 2
ColumnLayout { CustomRect {
Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
spacing: Tokens.spacing.medium topLeftRadius: Tokens.rounding.largeIncreased
bottomLeftRadius: Tokens.rounding.largeIncreased
color: Colors.tPalette.m3surfaceContainer
radius: Tokens.rounding.small
CustomRect { UserDock {
Layout.fillWidth: true greeter: root.greeter
color: Colors.tPalette.m3surfaceContainer
implicitHeight: weather.implicitHeight
radius: Tokens.rounding.small
topLeftRadius: Tokens.rounding.largeIncreased
WeatherInfo {
id: weather
rootHeight: root.height
}
}
CustomClippingRect {
Layout.fillHeight: true
Layout.fillWidth: true
bottomLeftRadius: Tokens.rounding.largeIncreased
color: Colors.tPalette.m3surfaceContainer
radius: Tokens.rounding.small
UserDock {
greeter: root.greeter
}
} }
} }
@@ -49,21 +31,16 @@ RowLayout {
screenHeight: root.screenHeight screenHeight: root.screenHeight
} }
ColumnLayout { CustomRect {
Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
spacing: Tokens.spacing.medium bottomRightRadius: Tokens.rounding.largeIncreased
color: Colors.tPalette.m3surfaceContainer
radius: Tokens.rounding.small
topRightRadius: Tokens.rounding.largeIncreased
CustomRect { SessionDock {
Layout.fillHeight: true greeter: root.greeter
Layout.fillWidth: true
bottomRightRadius: Tokens.rounding.largeIncreased
color: Colors.tPalette.m3surfaceContainer
radius: Tokens.rounding.small
topRightRadius: Tokens.rounding.largeIncreased
SessionDock {
greeter: root.greeter
}
} }
} }
} }
+1 -2
View File
@@ -134,8 +134,7 @@ Scope {
id: lockIcon id: lockIcon
anchors.centerIn: parent anchors.centerIn: parent
font.bold: true fontStyle: Tokens.font.icon.builders.extraLarge.size(Tokens.font.icon.extraLarge.pointSize * 3).build()
font.pointSize: Tokens.font.size.extraLarge * 4
text: "lock" text: "lock"
} }
+8 -15
View File
@@ -39,8 +39,7 @@ Item {
anchors.centerIn: parent anchors.centerIn: parent
animate: true animate: true
color: root.greeter.launching ? Colors.palette.m3secondary : Colors.palette.m3outline color: root.greeter.launching ? Colors.palette.m3secondary : Colors.palette.m3outline
font.family: Config.appearance.font.family.mono font: Tokens.font.mono.medium
font.pointSize: Tokens.font.size.normal
opacity: root.buffer ? 0 : 1 opacity: root.buffer ? 0 : 1
text: { text: {
if (root.greeter.launching) if (root.greeter.launching)
@@ -51,8 +50,7 @@ Item {
} }
Behavior on opacity { Behavior on opacity {
Anim { Anim {}
}
} }
} }
@@ -64,15 +62,13 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
color: Colors.palette.m3onSurface color: Colors.palette.m3onSurface
elide: Text.ElideLeft elide: Text.ElideLeft
font.family: Config.appearance.font.family.mono font: Tokens.font.mono.medium
font.pointSize: Tokens.font.size.normal
horizontalAlignment: Qt.AlignHCenter horizontalAlignment: Qt.AlignHCenter
opacity: root.greeter.echoResponse && root.buffer ? 1 : 0 opacity: root.greeter.echoResponse && root.buffer ? 1 : 0
text: root.buffer text: root.buffer
Behavior on opacity { Behavior on opacity {
Anim { Anim {}
}
} }
} }
@@ -89,7 +85,7 @@ Item {
anchors.centerIn: parent anchors.centerIn: parent
anchors.horizontalCenterOffset: implicitWidth > root.width ? -(implicitWidth - root.width) / 2 : 0 anchors.horizontalCenterOffset: implicitWidth > root.width ? -(implicitWidth - root.width) / 2 : 0
implicitHeight: Tokens.font.size.normal implicitHeight: Tokens.font.body.medium
implicitWidth: fullWidth implicitWidth: fullWidth
interactive: false interactive: false
opacity: root.greeter.echoResponse ? 0 : 1 opacity: root.greeter.echoResponse ? 0 : 1
@@ -107,8 +103,7 @@ Item {
scale: 0 scale: 0
Behavior on opacity { Behavior on opacity {
Anim { Anim {}
}
} }
Behavior on scale { Behavior on scale {
Anim { Anim {
@@ -156,15 +151,13 @@ Item {
Behavior on implicitWidth { Behavior on implicitWidth {
id: imWidthBehavior id: imWidthBehavior
Anim { Anim {}
}
} }
model: ScriptModel { model: ScriptModel {
values: root.buffer.split("") values: root.buffer.split("")
} }
Behavior on opacity { Behavior on opacity {
Anim { Anim {}
}
} }
} }
} }
+11 -17
View File
@@ -13,22 +13,19 @@ ColumnLayout {
anchors.fill: parent anchors.fill: parent
anchors.margins: Tokens.padding.large anchors.margins: Tokens.padding.large
spacing: Tokens.spacing.small spacing: Tokens.spacing.large
CustomText { CustomText {
Layout.fillWidth: true Layout.fillWidth: true
color: Colors.palette.m3outline color: Colors.palette.m3outline
elide: Text.ElideRight elide: Text.ElideRight
font.family: Config.appearance.font.family.mono font: Tokens.font.mono.builders.small.weight(Font.Medium).build()
font.weight: 500
text: root.greeter.sessions.length > 0 ? qsTr("%1 session%2").arg(root.greeter.sessions.length).arg(root.greeter.sessions.length === 1 ? "" : "s") : qsTr("Sessions") text: root.greeter.sessions.length > 0 ? qsTr("%1 session%2").arg(root.greeter.sessions.length).arg(root.greeter.sessions.length === 1 ? "" : "s") : qsTr("Sessions")
} }
CustomClippingRect { 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
@@ -46,16 +43,14 @@ ColumnLayout {
MaterialIcon { MaterialIcon {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
color: Colors.palette.m3outlineVariant color: Colors.palette.m3outlineVariant
font.pointSize: Tokens.font.size.extraLarge * 2 fontStyle: Tokens.font.icon.large
text: "desktop_windows" text: "desktop_windows"
} }
CustomText { CustomText {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
color: Colors.palette.m3outlineVariant color: Colors.palette.m3outlineVariant
font.family: Config.appearance.font.family.mono font: Tokens.font.headline.builders.small.width(90).build()
font.pointSize: Tokens.font.size.large
font.weight: 500
text: qsTr("No Sessions Found") text: qsTr("No Sessions Found")
} }
} }
@@ -68,6 +63,7 @@ ColumnLayout {
clip: true clip: true
currentIndex: root.greeter.sessionIndex currentIndex: root.greeter.sessionIndex
highlightFollowsCurrentItem: false highlightFollowsCurrentItem: false
boundsBehavior: ListView.StopAtBounds
model: root.greeter.sessions model: root.greeter.sessions
spacing: Tokens.spacing.small spacing: Tokens.spacing.small
@@ -80,7 +76,7 @@ ColumnLayout {
anchors.left: parent?.left anchors.left: parent?.left
anchors.right: parent?.right anchors.right: parent?.right
implicitHeight: row.implicitHeight + Tokens.padding.small * 2 implicitHeight: row.implicitHeight + Tokens.padding.small * 2
radius: Tokens.rounding.medium - Tokens.padding.small radius: Tokens.rounding.large
StateLayer { StateLayer {
onClicked: { onClicked: {
@@ -97,7 +93,7 @@ ColumnLayout {
MaterialIcon { MaterialIcon {
color: session.index === sessions.currentIndex ? Colors.palette.m3onPrimary : Colors.palette.m3onSurfaceVariant color: session.index === sessions.currentIndex ? Colors.palette.m3onPrimary : Colors.palette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.extraLarge fontStyle: Tokens.font.icon.extraLarge
text: modelData.kind === "x11" ? "tv" : "desktop_windows" text: modelData.kind === "x11" ? "tv" : "desktop_windows"
} }
@@ -109,8 +105,7 @@ ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
color: session.index === sessions.currentIndex ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface color: session.index === sessions.currentIndex ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
elide: Text.ElideRight elide: Text.ElideRight
font.pointSize: Tokens.font.size.normal font: Tokens.font.body.builders.medium.weight(Font.Bold).build()
font.weight: 600
text: modelData.name text: modelData.name
} }
@@ -118,8 +113,7 @@ ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
color: session.index === sessions.currentIndex ? Colors.palette.m3onPrimaryFixedVariant : Colors.palette.m3onSurfaceVariant color: session.index === sessions.currentIndex ? Colors.palette.m3onPrimaryFixedVariant : Colors.palette.m3onSurfaceVariant
elide: Text.ElideRight elide: Text.ElideRight
font.family: Config.appearance.font.family.mono font: Tokens.font.mono.small
font.pointSize: Tokens.font.size.small
text: modelData.kind text: modelData.kind
} }
} }
@@ -129,7 +123,7 @@ ColumnLayout {
color: Colors.palette.m3primary color: Colors.palette.m3primary
implicitHeight: sessions.currentItem?.implicitHeight ?? 0 implicitHeight: sessions.currentItem?.implicitHeight ?? 0
implicitWidth: sessions.width implicitWidth: sessions.width
radius: Tokens.rounding.medium - Tokens.padding.small radius: Tokens.rounding.large
y: sessions.currentItem?.y ?? 0 y: sessions.currentItem?.y ?? 0
Behavior on y { Behavior on y {
+11 -16
View File
@@ -14,22 +14,19 @@ ColumnLayout {
anchors.fill: parent anchors.fill: parent
anchors.margins: Tokens.padding.large anchors.margins: Tokens.padding.large
spacing: Tokens.spacing.small spacing: Tokens.spacing.large
CustomText { CustomText {
Layout.fillWidth: true Layout.fillWidth: true
color: Colors.palette.m3outline color: Colors.palette.m3outline
elide: Text.ElideRight elide: Text.ElideRight
font.family: Config.appearance.font.family.mono font: Tokens.font.mono.builders.small.weight(Font.Medium).build()
font.weight: 500
text: root.greeter.users.length > 0 ? qsTr("%1 user%2").arg(root.greeter.users.length).arg(root.greeter.users.length === 1 ? "" : "s") : qsTr("Users") text: root.greeter.users.length > 0 ? qsTr("%1 user%2").arg(root.greeter.users.length).arg(root.greeter.users.length === 1 ? "" : "s") : qsTr("Users")
} }
CustomClippingRect { 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
@@ -48,16 +45,14 @@ ColumnLayout {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
color: Colors.palette.m3outlineVariant color: Colors.palette.m3outlineVariant
fill: 1 fill: 1
font.pointSize: Tokens.font.size.extraLarge * 2 fontStyle: Tokens.font.icon.large
text: "account_circle" text: "account_circle"
} }
CustomText { CustomText {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
color: Colors.palette.m3outlineVariant color: Colors.palette.m3outlineVariant
font.family: Config.appearance.font.family.mono font: Tokens.font.headline.builders.small.width(90).build()
font.pointSize: Tokens.font.size.large
font.weight: 500
text: qsTr("No Users Found") text: qsTr("No Users Found")
} }
} }
@@ -70,6 +65,7 @@ ColumnLayout {
clip: true clip: true
currentIndex: root.greeter.selectedUser ? root.greeter.users.indexOf(root.greeter.selectedUser) : -1 currentIndex: root.greeter.selectedUser ? root.greeter.users.indexOf(root.greeter.selectedUser) : -1
highlightFollowsCurrentItem: false highlightFollowsCurrentItem: false
boundsBehavior: ListView.StopAtBounds
model: root.greeter.users model: root.greeter.users
spacing: Tokens.spacing.small spacing: Tokens.spacing.small
@@ -82,7 +78,7 @@ ColumnLayout {
anchors.left: parent?.left anchors.left: parent?.left
anchors.right: parent?.right anchors.right: parent?.right
implicitHeight: row.implicitHeight + Tokens.padding.small * 2 implicitHeight: row.implicitHeight + Tokens.padding.small * 2
radius: Tokens.rounding.medium - Tokens.padding.small radius: Tokens.rounding.large
StateLayer { StateLayer {
onClicked: { onClicked: {
@@ -97,7 +93,7 @@ ColumnLayout {
anchors.margins: Tokens.padding.small anchors.margins: Tokens.padding.small
spacing: Tokens.spacing.medium spacing: Tokens.spacing.medium
CustomClippingRect { ClipRect {
Layout.preferredHeight: 50 Layout.preferredHeight: 50
Layout.preferredWidth: 50 Layout.preferredWidth: 50
radius: Tokens.rounding.full radius: Tokens.rounding.full
@@ -106,7 +102,7 @@ ColumnLayout {
anchors.centerIn: parent anchors.centerIn: parent
color: user.index === users.currentIndex ? Colors.palette.m3onPrimary : Colors.palette.m3onSurfaceVariant color: user.index === users.currentIndex ? Colors.palette.m3onPrimary : Colors.palette.m3onSurfaceVariant
fill: 1 fill: 1
font.pointSize: Tokens.font.size.extraLarge fontStyle: Tokens.font.icon.extraLarge
text: modelData.kind === "x11" ? "tv" : "account_circle" text: modelData.kind === "x11" ? "tv" : "account_circle"
} }
@@ -124,8 +120,7 @@ ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
color: user.index === users.currentIndex ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface color: user.index === users.currentIndex ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
elide: Text.ElideRight elide: Text.ElideRight
font.pointSize: Tokens.font.size.normal font: Tokens.font.body.builders.medium.weight(Font.Bold).build()
font.weight: 600
text: modelData.username text: modelData.username
} }
} }
@@ -135,7 +130,7 @@ ColumnLayout {
color: Colors.palette.m3primary color: Colors.palette.m3primary
implicitHeight: users.currentItem?.implicitHeight ?? 0 implicitHeight: users.currentItem?.implicitHeight ?? 0
implicitWidth: users.width implicitWidth: users.width
radius: Tokens.rounding.medium - Tokens.padding.small radius: Tokens.rounding.large
y: users.currentItem?.y ?? 0 y: users.currentItem?.y ?? 0
Behavior on y { Behavior on y {
-171
View File
@@ -1,171 +0,0 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import ZShell.Config
import qs.Components
import qs.Helpers
import qs.Services
ColumnLayout {
id: root
required property int rootHeight
anchors.left: parent.left
anchors.margins: Tokens.padding.large * 2
anchors.right: parent.right
spacing: Tokens.spacing.small
Loader {
Layout.alignment: Qt.AlignHCenter
Layout.bottomMargin: -Tokens.padding.large
Layout.topMargin: Tokens.padding.large * 2
active: root.rootHeight > 610
visible: active
sourceComponent: CustomText {
color: Colors.palette.m3primary
font.pointSize: Tokens.font.size.extraLarge
font.weight: 500
text: qsTr("Weather")
}
}
RowLayout {
Layout.fillWidth: true
spacing: Tokens.spacing.largeIncreased
MaterialIcon {
animate: true
color: Colors.palette.m3secondary
font.pointSize: Tokens.font.size.extraLarge * 2.5
text: Weather.icon
}
ColumnLayout {
spacing: Tokens.spacing.small
CustomText {
Layout.fillWidth: true
animate: true
color: Colors.palette.m3secondary
elide: Text.ElideRight
font.pointSize: Tokens.font.size.large
font.weight: 500
text: Weather.description
}
CustomText {
Layout.fillWidth: true
animate: true
color: Colors.palette.m3onSurfaceVariant
elide: Text.ElideRight
font.pointSize: Tokens.font.size.normal
text: qsTr("Humidity: %1%").arg(Weather.humidity)
}
}
Loader {
Layout.rightMargin: Tokens.padding.small
active: root.width > 400
visible: active
sourceComponent: ColumnLayout {
spacing: Tokens.spacing.small
CustomText {
Layout.fillWidth: true
animate: true
color: Colors.palette.m3primary
elide: Text.ElideLeft
font.pointSize: Tokens.font.size.extraLarge
font.weight: 500
horizontalAlignment: Text.AlignRight
text: Weather.temp
}
CustomText {
Layout.fillWidth: true
animate: true
color: Colors.palette.m3outline
elide: Text.ElideLeft
font.pointSize: Tokens.font.size.smaller
horizontalAlignment: Text.AlignRight
text: qsTr("Feels like: %1").arg(Weather.feelsLike)
}
}
}
}
Loader {
id: forecastLoader
Layout.bottomMargin: Tokens.padding.large * 2
Layout.fillWidth: true
Layout.topMargin: Tokens.spacing.small
active: root.rootHeight > 820
visible: active
sourceComponent: RowLayout {
spacing: Tokens.spacing.largeIncreased
Repeater {
model: {
const forecast = Weather.hourlyForecast;
const count = root.width < 320 ? 3 : root.width < 400 ? 4 : 5;
if (!forecast)
return Array.from({
length: count
}, () => null);
const start = Math.max(0, Weather.currentHourIndex);
return forecast.slice(start, start + count);
}
ColumnLayout {
id: forecastHour
required property var modelData
Layout.fillWidth: true
spacing: Tokens.spacing.small
CustomText {
Layout.fillWidth: true
color: Colors.palette.m3outline
font.pointSize: Tokens.font.size.larger
horizontalAlignment: Text.AlignHCenter
text: {
const hour = forecastHour.modelData?.hour ?? 0;
return hour > 12 ? `${(hour - 12).toString().padStart(2, "0")} PM` : `${hour.toString().padStart(2, "0")} AM`;
}
}
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
font.pointSize: Tokens.font.size.extraLarge * 1.5
font.weight: 500
text: forecastHour.modelData?.icon ?? "cloud_alert"
}
CustomText {
Layout.alignment: Qt.AlignHCenter
color: Colors.palette.m3secondary
font.pointSize: Tokens.font.size.larger
text: Config.services.useFahrenheit ? `${forecastHour.modelData?.tempF ?? 0}°F` : `${forecastHour.modelData?.tempC ?? 0}°C`
}
}
}
}
}
Timer {
interval: 900000 // 15 minutes
repeat: true
running: true
triggeredOnStart: true
onTriggered: Weather.reload()
}
}
+105 -43
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.Internal import ZShell.Wallpaper
import ZShell.Config import ZShell.Config
import qs.Components import qs.Components
import qs.Services import qs.Services
@@ -16,24 +16,48 @@ 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
property alias monitorCrops: monitorCrops readonly property string currentThumbnail: thumbnailFor(current)
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: FileSystemEntry): string { function getCategoryFor(w: WallpaperEntry): 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 root.crops[screen]; return wallpapers.crop(root.current, screen);
} }
function preview(path: string): void { function preview(path: string): void {
@@ -50,19 +74,18 @@ Searcher {
else if (zoom > 5.0) else if (zoom > 5.0)
zoom = 5.0; zoom = 5.0;
root.crops[screen] = { wallpapers.setCrop(root.current, 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}`]);
@@ -96,38 +119,13 @@ Searcher {
target: "wallpaper" target: "wallpaper"
} }
FileView { WallpaperDb {
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
filter: FileSystemModel.Images dbPath: `${Paths.state}/wallpapers.sqlite`
cacheSize: wallpaper.largestBufferSize
requireHardwareCodec: true
path: Config.general.wallpaperPath path: Config.general.wallpaperPath
recursive: true
} }
Process { Process {
@@ -146,36 +144,100 @@ 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 {
readonly property var crops: root.getCrop(modelData.name) id: obj
required property var modelData required property var modelData
cropHeight: crops?.height ?? 1.0 // Crop animation only for edits, never for wallpaper swaps.
cropWidth: crops?.width ?? 1.0 property bool animateCrop: false
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();
}
}
}
} }
+1 -2
View File
@@ -210,8 +210,7 @@ GridLayout {
roleValue: "network" roleValue: "network"
delegate: EntryWrapper { delegate: EntryWrapper {
NetworkWidget { NetworkWidget {}
}
} }
} }
-49
View File
@@ -1,49 +0,0 @@
pragma ComponentBehavior: Bound
import Quickshell
import QtQuick
import QtQuick.Effects
import ZShell.Config
import qs.Components
import qs.Services
Item {
id: root
required property Item bar
required property ScreenState screenState
anchors.fill: parent
CustomRect {
anchors.fill: parent
anchors.margins: -1
color: Colors.palette.m3surface
layer.enabled: true
layer.effect: MultiEffect {
maskEnabled: true
maskInverted: true
maskSource: mask
maskSpreadAtMin: 1
maskThresholdMin: 0.5
}
}
Item {
id: mask
anchors.fill: parent
layer.enabled: true
visible: false
Rectangle {
anchors.fill: parent
anchors.margins: Config.bar.border + 1
anchors.topMargin: root.bar.implicitHeight + 1
radius: Config.bar.border > 0 ? Config.bar.rounding : 0
topLeftRadius: Config.bar.rounding
topRightRadius: Config.bar.rounding
}
}
}
+3 -1
View File
@@ -45,7 +45,9 @@ WidgetBase {
} }
required property GridLayout loader required property GridLayout loader
required property Wrapper popouts required property Wrapper popouts
readonly property real size: horizontal ? timeText.contentWidth + Tokens.padding.medium * 2 : verticalColumn.implicitHeight + Tokens.padding.medium * 2 readonly property int roundedWidth: Math.round(timeText.implicitWidth)
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 width: root.batWidth - 1
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 + Tokens.padding.small * 2 : baseSize implicitWidth: horizontal ? (layout.implicitWidth % 2 === 0 ? layout.implicitWidth : layout.implicitWidth + 1) + Tokens.padding.small * 2 : baseSize
radius: Tokens.rounding.full radius: Tokens.rounding.full
Behavior on implicitHeight { Behavior on implicitHeight {
+8 -12
View File
@@ -12,7 +12,6 @@ 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
@@ -23,17 +22,12 @@ 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
@@ -59,7 +53,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: Math.min(width, height) / 2 radius: Tokens.rounding.full
Item { Item {
id: clipper id: clipper
@@ -69,18 +63,20 @@ 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
height: root.horizontal ? root.snap(root.percentage * track.height) : track.height * 2 implicitHeight: root.horizontal ? root.percentage * track.height : track.height * 2
width: root.horizontal ? track.width * 2 : root.snap(root.percentage * track.width) implicitWidth: root.horizontal ? track.width * 2 : 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,6 +33,7 @@ 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 {
@@ -51,6 +52,7 @@ 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
+8 -9
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.medium / 2 readonly property int spacing: Tokens.spacing.extraSmall
// 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,14 +51,12 @@ WidgetBase {
Behavior on implicitHeight { Behavior on implicitHeight {
enabled: !root.horizontal enabled: !root.horizontal
Anim { Anim {}
}
} }
Behavior on implicitWidth { Behavior on implicitWidth {
enabled: root.horizontal enabled: root.horizontal
Anim { Anim {}
}
} }
GridLayout { GridLayout {
@@ -70,6 +68,8 @@ 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,8 +111,7 @@ WidgetBase {
delegate: EntryWrapper { delegate: EntryWrapper {
name: "upower" name: "upower"
UPowerWidget { UPowerWidget {}
}
} }
} }
} }
@@ -135,8 +134,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?.implicitHeight ?? 0 implicitHeight: item?.height ?? item?.implicitHeight ?? 0
implicitWidth: item?.implicitWidth ?? 0 implicitWidth: item?.width ?? 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: speaker id: root
required property bool horizontal required property bool horizontal
@@ -15,6 +15,8 @@ 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: mic id: root
required property bool horizontal required property bool horizontal
@@ -15,6 +15,8 @@ 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
@@ -79,6 +79,7 @@ Item {
anchors.centerIn: parent anchors.centerIn: parent
antialiasing: true antialiasing: true
backer.retainWhileLoading: true
color: root.current && !Config.bar.tray.showOnHover ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface color: root.current && !Config.bar.tray.showOnHover ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
implicitSize: Config.bar.tray.trayIconSize * root.dpr implicitSize: Config.bar.tray.trayIconSize * root.dpr
layer.enabled: Config.bar.tray.recolorIcons layer.enabled: Config.bar.tray.recolorIcons
+1
View File
@@ -38,6 +38,7 @@ 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
+25 -27
View File
@@ -12,13 +12,14 @@ import qs.Services
Item { Item {
id: root id: root
property real activeWorkspaceMargin: Math.ceil(Tokens.padding.medium / 4) property real activeWorkspaceMargin: Tokens.padding.extraSmall / 2
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) + activeWorkspaceMargin * 2 readonly property real size: (workspaceButtonWidth * workspacesShown) + (spacing * Math.max(0, workspacesShown - 1)) + 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)
@@ -59,15 +60,17 @@ Item {
anchors.fill: parent anchors.fill: parent
anchors.margins: root.activeWorkspaceMargin anchors.margins: root.activeWorkspaceMargin
columnSpacing: 0 columnSpacing: root.spacing
columns: root.horizontal ? root.workspacesShown : 1 columns: root.horizontal ? root.workspacesShown : 1
rowSpacing: 0 rowSpacing: root.spacing
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
Button { CustomRect {
id: button id: button
required property int index required property int index
@@ -75,27 +78,19 @@ Item {
implicitHeight: indicator.indicatorThickness implicitHeight: indicator.indicatorThickness
implicitWidth: indicator.indicatorThickness implicitWidth: indicator.indicatorThickness
width: root.workspaceButtonWidth radius: Tokens.rounding.full
background: Item { CustomText {
id: workspaceButtonBackground anchors.centerIn: parent
color: Colors.palette.m3onSecondaryContainer
implicitHeight: root.workspaceButtonWidth text: button.modelData?.name ?? ""
implicitWidth: root.workspaceButtonWidth
CustomText {
anchors.centerIn: parent
color: Colors.palette.m3onSecondaryContainer
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
text: button.modelData?.name ?? ""
verticalAlignment: Text.AlignVCenter
}
} }
onPressed: { StateLayer {
const ws = button.modelData.name; onPressed: {
Hyprland.dispatch(Hyprland.usingLua ? `hl.dsp.focus({ workspace= "${ws}"})` : `workspace ${ws}`); const ws = button.modelData.name;
Hyprland.dispatch(Hyprland.usingLua ? `hl.dsp.focus({ workspace= "${ws}"})` : `workspace ${ws}`);
}
} }
} }
} }
@@ -104,8 +99,10 @@ Item {
CustomRect { CustomRect {
id: indicator id: indicator
property real indicatorLength: (Math.abs(idxPair.idx1 - idxPair.idx2) + 1) * root.workspaceButtonWidth readonly property real step: root.workspaceButtonWidth + root.spacing
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
@@ -129,9 +126,10 @@ 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 + 3 : 0 x: root.horizontal ? -indicator.x + root.activeWorkspaceMargin : 0
y: root.horizontal ? 0 : -indicator.y + 3 y: root.horizontal ? 0 : -indicator.y + root.activeWorkspaceMargin
} }
} }
} }
+2 -2
View File
@@ -134,7 +134,7 @@ Item {
node: Audio.source node: Audio.source
} }
CustomClippingRect { ClipRect {
anchors.fill: parent anchors.fill: parent
radius: parent.radius radius: parent.radius
@@ -249,7 +249,7 @@ Item {
node: appBox.modelData node: appBox.modelData
} }
CustomClippingRect { ClipRect {
anchors.fill: parent anchors.fill: parent
radius: parent.radius radius: parent.radius
+5 -3
View File
@@ -25,7 +25,7 @@ Item {
searchField.forceActiveFocus(); searchField.forceActiveFocus();
} }
CustomClippingRect { ClipRect {
id: search id: search
anchors.left: parent.left anchors.left: parent.left
@@ -54,7 +54,7 @@ Item {
} }
} }
CustomClippingRect { CustomRect {
id: preview id: preview
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
@@ -64,6 +64,7 @@ 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 {}
@@ -228,11 +229,12 @@ Item {
anchors.fill: parent anchors.fill: parent
spacing: Tokens.spacing.small spacing: Tokens.spacing.small
CustomClippingRect { Item {
id: textRect id: textRect
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
clip: true
Item { Item {
id: textWrapper id: textWrapper
+1 -1
View File
@@ -14,7 +14,7 @@ Row {
padding: 20 padding: 20
spacing: 12 spacing: 12
CustomClippingRect { ClipRect {
color: Colors.layer(Colors.palette.m3surfaceContainerHigh, 2) color: Colors.layer(Colors.palette.m3surfaceContainerHigh, 2)
implicitHeight: info.implicitHeight implicitHeight: info.implicitHeight
implicitWidth: info.implicitHeight implicitWidth: info.implicitHeight
+1 -1
View File
@@ -16,7 +16,7 @@ Item {
implicitWidth: 1000 implicitWidth: 1000
implicitHeight: 320 implicitHeight: 320
CustomClippingRect { ClipRect {
anchors.fill: parent anchors.fill: parent
radius: root.radius radius: root.radius
@@ -46,7 +46,7 @@ CustomListView {
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
spacing: Tokens.spacing.extraSmall / 2 spacing: Tokens.spacing.extraSmall / 2
CustomClippingRect { ClipRect {
id: iconClip id: iconClip
Layout.fillHeight: true Layout.fillHeight: true
@@ -25,7 +25,7 @@ Item {
onClicked: root.close() onClicked: root.close()
} }
CustomClippingRect { ClipRect {
id: popupBackground id: popupBackground
readonly property real padding: 4 readonly property real padding: 4
@@ -40,7 +40,7 @@ Item {
visible: false visible: false
z: 999 z: 999
CustomClippingRect { ClipRect {
id: popupBackground id: popupBackground
readonly property real padding: Tokens.padding.extraSmall readonly property real padding: Tokens.padding.extraSmall
+61 -9
View File
@@ -1,3 +1,5 @@
pragma ComponentBehavior: Bound
import Quickshell import Quickshell
import QtQuick import QtQuick
import ZShell.Config import ZShell.Config
@@ -9,7 +11,7 @@ Item {
id: root id: root
required property bool isCurrent required property bool isCurrent
required property var modelData // FileSystemEntry-like {path, relativePath} required property var modelData
required property real tileHeight required property real tileHeight
required property real tileWidth required property real tileWidth
required property ScreenState screenState required property ScreenState screenState
@@ -19,7 +21,7 @@ Item {
implicitHeight: tileHeight + Tokens.padding.large * 2 implicitHeight: tileHeight + Tokens.padding.large * 2
implicitWidth: tileWidth implicitWidth: tileWidth
CustomClippingRect { ClipRect {
id: image id: image
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
@@ -29,25 +31,75 @@ Item {
radius: Tokens.rounding.small radius: Tokens.rounding.small
y: Tokens.padding.large y: Tokens.padding.large
MaterialIcon { Loader {
active: opacity > 0
anchors.centerIn: parent anchors.centerIn: parent
color: Colors.tPalette.m3outline opacity: img.status === Image.Ready ? 0 : 1
fontStyle: Tokens.font.icon.builders.extraLarge.scale(2).weight(Font.DemiBold).build()
text: "image" Behavior on opacity {
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
}
}
} }
CachingImage { Image {
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
cache: true asynchronous: true
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
height: root.tileHeight height: root.tileHeight
path: root.modelData ? root.modelData.path : "" source: root.modelData ? root.modelData.thumbnailPath : ""
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 {
+1 -1
View File
@@ -55,7 +55,7 @@ ColumnLayout {
text: Time.format("dddd, d MMMM yyyy") text: Time.format("dddd, d MMMM yyyy")
} }
CustomClippingRect { ClipRect {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Layout.topMargin: Tokens.spacing.largeIncreased * 2 Layout.topMargin: Tokens.spacing.largeIncreased * 2
color: Colors.tPalette.m3surfaceContainer color: Colors.tPalette.m3surfaceContainer
+4 -17
View File
@@ -5,14 +5,13 @@ import qs.Helpers
import ZShell.Config import ZShell.Config
import qs.Services import qs.Services
CustomClippingRect { Item {
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
@@ -50,22 +49,10 @@ CustomClippingRect {
lock: root.lock lock: root.lock
} }
ColumnLayout { NotifDock {
Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
spacing: Tokens.spacing.medium lock: root.lock
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
+32 -27
View File
@@ -2,44 +2,49 @@ import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell import Quickshell
import ZShell.Components import ZShell.Components
import qs.Components
import ZShell.Config import ZShell.Config
import qs.Components
import qs.Helpers import qs.Helpers
import qs.Services import qs.Services
CustomClippingRect { Item {
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);
}
Behavior on opacity { color: Colors.tPalette.m3surfaceContainer
Anim { radius: Tokens.rounding.small
type: Anim.StandardExtraLarge bottomLeftRadius: Tokens.rounding.largeIncreased
}
} FadeImage {
id: image
CustomRect {
anchors.fill: parent anchors.fill: parent
color: Colors.palette.m3surface asynchronous: true
opacity: 0.7 fillMode: Image.PreserveAspectCrop
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
}
} }
} }
@@ -77,7 +82,7 @@ CustomClippingRect {
spacing: Tokens.spacing.extraSmall spacing: Tokens.spacing.extraSmall
IconButton { IconButton {
enabled: Players.active?.canGoPrevious enabled: Players.active?.canGoPrevious ?? false
icon: "skip_previous" icon: "skip_previous"
isRound: true isRound: true
shapeMorph: true shapeMorph: true
@@ -88,7 +93,7 @@ CustomClippingRect {
IconButton { IconButton {
checked: Players.active?.isPlaying ?? false checked: Players.active?.isPlaying ?? false
enabled: Players.active?.canTogglePlaying enabled: Players.active?.canTogglePlaying ?? false
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
@@ -98,7 +103,7 @@ CustomClippingRect {
} }
IconButton { IconButton {
enabled: Players.active?.canGoNext enabled: Players.active?.canGoNext ?? false
icon: "skip_next" icon: "skip_next"
isRound: true isRound: true
shapeMorph: true shapeMorph: true
+94 -105
View File
@@ -11,127 +11,116 @@ import ZShell.Config
import qs.Daemons import qs.Daemons
import qs.Services import qs.Services
ColumnLayout { CustomRect {
id: root id: root
required property var lock required property var lock
anchors.fill: parent bottomRightRadius: Tokens.rounding.largeIncreased
anchors.margins: Tokens.padding.large color: Colors.tPalette.m3surfaceContainer
spacing: Tokens.spacing.small radius: Tokens.rounding.small
topRightRadius: Tokens.rounding.largeIncreased
CustomText { ColumnLayout {
Layout.fillWidth: true anchors.fill: parent
color: Colors.palette.m3outline anchors.margins: Tokens.padding.large
elide: Text.ElideRight spacing: Tokens.spacing.small
font.family: Config.appearance.font.family.mono
font.weight: 500
text: NotifServer.list.length > 0 ? qsTr("%1 notification%2").arg(NotifServer.list.length).arg(NotifServer.list.length === 1 ? "" : "s") : qsTr("Notifications")
}
ClippingRectangle { CustomText {
id: clipRect Layout.fillWidth: true
color: Colors.palette.m3outline
Layout.fillHeight: true elide: Text.ElideRight
Layout.fillWidth: true font: Tokens.font.mono.builders.small.weight(Font.Medium).build()
color: "transparent" text: NotifServer.list.length > 0 ? qsTr("%1 notification%2").arg(NotifServer.list.length).arg(NotifServer.list.length === 1 ? "" : "s") : qsTr("Notifications")
radius: Tokens.rounding.small
Loader {
active: opacity > 0
anchors.centerIn: parent
opacity: NotifServer.list.length > 0 ? 0 : 1
Behavior on opacity {
Anim {
duration: Tokens.anim.durations.extraLarge
}
}
sourceComponent: ColumnLayout {
spacing: Tokens.spacing.largeIncreased
Image {
asynchronous: true
fillMode: Image.PreserveAspectFit
layer.enabled: true
source: Qt.resolvedUrl(`${Quickshell.shellDir}/assets/dino.png`)
sourceSize.width: clipRect.width * 0.8
layer.effect: Coloriser {
brightness: 1
colorizationColor: Colors.palette.m3outlineVariant
}
}
CustomText {
Layout.alignment: Qt.AlignHCenter
color: Colors.palette.m3outlineVariant
font: Tokens.font.headline.builders.small.width(90).build()
text: qsTr("No Notifications")
}
}
} }
CustomListView { Item {
anchors.fill: parent Layout.fillHeight: true
clip: true Layout.fillWidth: true
spacing: Tokens.spacing.small
add: Transition { Loader {
Anim { active: opacity > 0
from: 0 anchors.centerIn: parent
property: "opacity" opacity: NotifServer.list.length > 0 ? 0 : 1
to: 1
Behavior on opacity {
Anim {
duration: Tokens.anim.durations.extraLarge
}
} }
sourceComponent: ColumnLayout {
spacing: Tokens.spacing.largeIncreased
Anim { CustomText {
duration: Tokens.anim.durations.expressiveDefaultSpatial Layout.alignment: Qt.AlignHCenter
easing: Tokens.anim.expressiveDefaultSpatial color: Colors.palette.m3outlineVariant
from: 0 font: Tokens.font.headline.builders.small.width(90).build()
property: "scale" text: qsTr("No Notifications")
to: 1 }
} }
} }
delegate: NotifGroup {}
displaced: 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
}
}
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 { add: Transition {
duration: Tokens.anim.durations.expressiveDefaultSpatial Anim {
easing: Tokens.anim.expressiveDefaultSpatial from: 0
property: "y" property: "opacity"
} to: 1
} }
remove: Transition {
Anim {
property: "opacity"
to: 0
}
Anim { Anim {
property: "scale" duration: Tokens.anim.durations.expressiveDefaultSpatial
to: 0.6 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
}
} }
} }
} }
+1 -1
View File
@@ -85,7 +85,7 @@ CustomRect {
} }
} }
ClippingRectangle { ClipRect {
anchors.fill: parent anchors.fill: parent
color: root.urgency === "critical" ? Colors.palette.m3error : root.urgency === "low" ? Colors.layer(Colors.palette.m3surfaceContainerHighest, 3) : Colors.palette.m3secondaryContainer color: root.urgency === "critical" ? Colors.palette.m3error : root.urgency === "low" ? Colors.layer(Colors.palette.m3surfaceContainerHighest, 3) : Colors.palette.m3secondaryContainer
radius: Tokens.rounding.full radius: Tokens.rounding.full
+2 -1
View File
@@ -1,12 +1,13 @@
import Quickshell import Quickshell
import Quickshell.Widgets import Quickshell.Widgets
import QtQuick import QtQuick
import qs.Components
import qs.Paths import qs.Paths
Item { Item {
id: root id: root
ClippingRectangle { ClipRect {
anchors.fill: parent anchors.fill: parent
radius: Tokens.rounding.full radius: Tokens.rounding.full
+4 -3
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.title.large font: Tokens.font.body.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.body.large font: Tokens.font.title.large
text: Weather.temp text: Weather.temp
} }
@@ -37,6 +37,7 @@ ColumnLayout {
animate: true animate: true
color: Colors.palette.m3secondary color: Colors.palette.m3secondary
text: Weather.icon text: Weather.icon
fontStyle: Tokens.font.icon.large
} }
} }
@@ -53,7 +54,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.normal font: Tokens.font.body.medium
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));
+12 -5
View File
@@ -36,7 +36,7 @@ CustomRect {
} }
} }
VerticalFadeListView { CustomListView {
id: layout id: layout
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
@@ -45,13 +45,20 @@ 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
model: Weather.hourlyForecast currentIndex: Weather.currentHourIndex
// 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
@@ -60,7 +67,7 @@ CustomRect {
MaterialShape { MaterialShape {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
color: Qt.alpha(Colors.palette.m3primary, hour.index === Weather.currentHourIndex ? 1 : 0) color: Qt.alpha(Colors.palette.m3primary, hour.current ? 1 : 0)
implicitSize: temp.implicitHeight + Tokens.padding.small * 2 implicitSize: temp.implicitHeight + Tokens.padding.small * 2
shape: MaterialShape.Cookie4Sided shape: MaterialShape.Cookie4Sided
@@ -72,7 +79,7 @@ CustomRect {
id: temp id: temp
anchors.centerIn: parent anchors.centerIn: parent
color: hour.index === Weather.currentHourIndex ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface color: hour.current ? 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
} }
@@ -96,7 +103,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.index === 0 ? qsTr("Now") : Qt.formatDateTime(new Date(hour.cond.timestamp.replace("T", " ")), Config.services.useTwelveHourClock ? "ha" : "hh:00") text: hour.current ? qsTr("Now") : Qt.formatDateTime(new Date(hour.cond.timestamp.replace("T", " ")), Config.services.useTwelveHourClock ? "ha" : "hh:00")
} }
} }
} }
+2 -2
View File
@@ -19,6 +19,7 @@ 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
@@ -48,7 +49,6 @@ CustomRect {
anchors.right: parent.right anchors.right: parent.right
asynchronous: true asynchronous: true
sourceComponent: Forecast { sourceComponent: Forecast {}
}
} }
} }
+4 -1
View File
@@ -28,7 +28,10 @@ Item {
for (let i = 0; i < count; i++) for (let i = 0; i < count; i++)
height += (list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0; height += (list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0;
if (panels.popouts.hasCurrent && (panels.popouts.currentCenter + (panels.popouts.current?.width / 2)) > panels.notifications.x || screenState.dashboard) if (panels.popouts.hasCurrent && (panels.popouts.currentCenter + (panels.popouts.current?.width / 2)) > panels.notifications.x)
return 0;
if (screenState.dashboard && (panels.dashboard.x + panels.dashboard.width >= panels.notifications.x))
return 0; return 0;
if (screenState.osd) { if (screenState.osd) {
+1 -1
View File
@@ -108,7 +108,7 @@ CustomRect {
visible: root.hasImage || root.hasAppIcon visible: root.hasImage || root.hasAppIcon
width: Config.notifs.sizes.image width: Config.notifs.sizes.image
sourceComponent: CustomClippingRect { sourceComponent: ClipRect {
color: root.modelData.urgency === NotificationUrgency.Critical ? Colors.palette.m3error : root.modelData.urgency === NotificationUrgency.Low ? Colors.layer(Colors.palette.m3surfaceContainerHighest, 2) : Colors.palette.m3secondaryContainer color: root.modelData.urgency === NotificationUrgency.Critical ? Colors.palette.m3error : root.modelData.urgency === NotificationUrgency.Low ? Colors.layer(Colors.palette.m3surfaceContainerHighest, 2) : Colors.palette.m3secondaryContainer
implicitHeight: Config.notifs.sizes.image implicitHeight: Config.notifs.sizes.image
implicitWidth: Config.notifs.sizes.image implicitWidth: Config.notifs.sizes.image
+1 -1
View File
@@ -5,7 +5,7 @@ import ZShell.Config
import qs.Components import qs.Components
import qs.Services import qs.Services
CustomClippingRect { ClipRect {
id: root id: root
property real animPerc: UPower.displayDevice.percentage property real animPerc: UPower.displayDevice.percentage
+1 -1
View File
@@ -19,7 +19,7 @@ CustomRect {
implicitWidth: 450 implicitWidth: 450
radius: Tokens.rounding.largeIncreased radius: Tokens.rounding.largeIncreased
CustomClippingRect { ClipRect {
anchors.fill: parent anchors.fill: parent
radius: parent.radius radius: parent.radius
+1 -1
View File
@@ -149,7 +149,7 @@ ColumnLayout {
} }
} }
Flickable { CustomFlickable {
id: flickable id: flickable
Layout.fillHeight: true Layout.fillHeight: true
+28 -4
View File
@@ -25,7 +25,7 @@ Item {
anchors.fill: parent anchors.fill: parent
spacing: Tokens.spacing.small spacing: Tokens.spacing.small
CustomClippingRect { ClipRect {
id: imgWrapper id: imgWrapper
Layout.fillWidth: true Layout.fillWidth: true
@@ -39,8 +39,7 @@ 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
@@ -64,18 +63,43 @@ 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
}
}
}
} }
} }
} }
+71 -35
View File
@@ -13,9 +13,14 @@ import qs.Services
Item { Item {
id: wrapper id: wrapper
property int buttonRowHeight: Tokens.padding.large * 3 readonly property bool isVideo: Wallpapers.currentIsVideo
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)
@@ -25,9 +30,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
@@ -100,29 +105,28 @@ Item {
visible: opacity > 0 visible: opacity > 0
Behavior on opacity { Behavior on opacity {
Anim { Anim {}
}
} }
Behavior on scale { Behavior on scale {
Anim { Anim {}
}
} }
Behavior on y { Behavior on y {
Anim { Anim {}
}
} }
Component.onCompleted: { Component.onCompleted: {
refreshScreens(); refreshScreens();
Qt.callLater(syncCropToScreen); Qt.callLater(syncCropToScreen);
} }
onCurrentWallpaperChanged: {
Qt.callLater(syncCropToScreen);
}
onSelectedScreenIndexChanged: { onSelectedScreenIndexChanged: {
Qt.callLater(syncCropToScreen); Qt.callLater(syncCropToScreen);
} }
ButtonRow { Loader {
id: screenSelector id: selectorLoader
anchors.bottom: imageSlot.top anchors.bottom: imageSlot.top
anchors.bottomMargin: Tokens.spacing.medium anchors.bottomMargin: Tokens.spacing.medium
anchors.left: parent.left anchors.left: parent.left
@@ -130,25 +134,32 @@ Item {
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: Tokens.padding.extraLarge * 2 anchors.rightMargin: Tokens.padding.extraLarge * 2
anchors.top: parent.top anchors.top: parent.top
spacing: Tokens.spacing.small
Repeater { active: wrapper.screens.length > 1 && !wrapper.isVideo
model: wrapper.screens
delegate: TextButton { sourceComponent: ButtonRow {
required property int index id: screenSelector
readonly property bool isCurrent: wrapper.selectedScreenIndex === index
required property var modelData
fillWidth: true spacing: Tokens.spacing.small
font: Tokens.font.body.builders.large.weight(Font.DemiBold).build()
inactiveColor: isCurrent ? Colors.palette.m3primary : Qt.alpha(Colors.palette.m3surfaceContainer, 0.7)
inactiveOnColor: isCurrent ? Colors.palette.m3onPrimary : Qt.alpha(Colors.palette.m3onSurface, 0.7)
isRound: true
shapeMorph: true
text: modelData.name
onClicked: wrapper.selectScreen(index) Repeater {
model: wrapper.screens
delegate: TextButton {
required property int index
readonly property bool isCurrent: wrapper.selectedScreenIndex === index
required property var modelData
fillWidth: true
font: Tokens.font.body.builders.large.weight(Font.DemiBold).build()
inactiveColor: isCurrent ? Colors.palette.m3primary : Qt.alpha(Colors.palette.m3surfaceContainer, 0.7)
inactiveOnColor: isCurrent ? Colors.palette.m3onPrimary : Qt.alpha(Colors.palette.m3onSurface, 0.7)
isRound: true
shapeMorph: true
text: modelData.name
onClicked: wrapper.selectScreen(index)
}
} }
} }
} }
@@ -160,6 +171,7 @@ 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
@@ -222,9 +234,9 @@ Item {
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.top: parent.top anchors.top: parent.top
anchors.topMargin: wrapper.buttonRowHeight + Tokens.spacing.medium anchors.topMargin: selectorLoader.active ? wrapper.buttonRowHeight + Tokens.spacing.medium : Tokens.spacing.medium
CustomClippingRect { ClipRect {
id: fitBox id: fitBox
readonly property real aspect: scaledImg.implicitHeight > 0 ? scaledImg.implicitWidth / scaledImg.implicitHeight : 1 readonly property real aspect: scaledImg.implicitHeight > 0 ? scaledImg.implicitWidth / scaledImg.implicitHeight : 1
@@ -271,7 +283,7 @@ Item {
opacity: status === Image.Ready ? 1 : 0 opacity: status === Image.Ready ? 1 : 0
retainWhileLoading: true retainWhileLoading: true
smooth: true smooth: true
source: Wallpapers.current source: Wallpapers.currentThumbnail
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;
@@ -282,12 +294,37 @@ 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 active: scaledImg.status === Image.Ready && !wrapper.isVideo
sourceComponent: Component { sourceComponent: Component {
CustomRect { CustomRect {
@@ -330,7 +367,6 @@ 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);
@@ -357,8 +393,7 @@ Item {
width: Math.round(baseWidth / zoom) width: Math.round(baseWidth / zoom)
Behavior on color { Behavior on color {
CAnim { CAnim {}
}
} }
Component.onCompleted: { Component.onCompleted: {
@@ -434,9 +469,10 @@ Item {
} }
anchors.fill: parent anchors.fill: parent
cursorShape: pressed ? Qt.ClosedHandCursor : insideCropRect(mouseX, mouseY) ? Qt.OpenHandCursor : Qt.PointingHandCursor cursorShape: wrapper.isVideo ? Qt.ArrowCursor : 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) {
+2
View File
@@ -38,6 +38,7 @@ Item {
} }
BlobInvertedRect { BlobInvertedRect {
id: blobRect
anchors.fill: parent anchors.fill: parent
borderBottom: Tokens.padding.small borderBottom: Tokens.padding.small
borderLeft: navPane.width + navPane.anchors.margins * 2 borderLeft: navPane.width + navPane.anchors.margins * 2
@@ -65,6 +66,7 @@ Item {
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.left: navPane.right anchors.left: navPane.right
anchors.leftMargin: navPane.anchors.margins + anchors.margins anchors.leftMargin: navPane.anchors.margins + anchors.margins
anchors.bottomMargin: blobRect.borderBottom
anchors.margins: Tokens.padding.extraLarge anchors.margins: Tokens.padding.extraLarge
anchors.right: parent.right anchors.right: parent.right
anchors.top: parent.top anchors.top: parent.top
-1
View File
@@ -38,7 +38,6 @@ 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,11 +50,12 @@ PageBase {
} }
WallItem { WallItem {
required property FileSystemEntry modelData id: wallItem
required property WallpaperEntry modelData
enabled: modelData enabled: modelData
opacity: modelData ? 1 : 0 opacity: modelData ? 1 : 0
source: String(modelData?.path ?? "") source: String(modelData?.thumbnailPath ?? "")
onClicked: { onClicked: {
Wallpapers.setWallpaper(modelData.path); Wallpapers.setWallpaper(modelData.path);
+48
View File
@@ -13,6 +13,42 @@ 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 {
@@ -73,6 +109,18 @@ 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
+53 -2
View File
@@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import Quickshell import Quickshell
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Services
import ZShell.Components import ZShell.Components
import ZShell.Config import ZShell.Config
import ZShell.Llm import ZShell.Llm
@@ -90,7 +91,9 @@ Item {
signal requestClose signal requestClose
readonly property bool atNewestEnd: !listLoader.item || listLoader.item.contentY < 4 readonly property bool atNewestEnd: !listLoader.item || listLoader.item.contentY < 4
readonly property bool importing: importIds.length > 0
property var importIds: []
property var pendingAttachments: [] property var pendingAttachments: []
function attachFiles(): void { function attachFiles(): void {
@@ -436,7 +439,7 @@ Item {
width: attachments.cellWidth width: attachments.cellWidth
height: width height: width
opacity: modelData ? 1 : 0 opacity: modelData ? 1 : 0
CustomClippingRect { ClipRect {
anchors.fill: parent anchors.fill: parent
anchors.margins: Tokens.spacing.extraSmall anchors.margins: Tokens.spacing.extraSmall
@@ -541,7 +544,7 @@ Item {
bg.border.color: Colors.palette.m3outlineVariant bg.border.color: Colors.palette.m3outlineVariant
bg.color: Colors.tPalette.m3surfaceContainerLowest bg.color: Colors.tPalette.m3surfaceContainerLowest
fastGlow: LlmClient.busy fastGlow: LlmClient.busy
glow.opacity: input.activeFocus && (!LlmClient.busy || ChatState.isWindow) glow.opacity: !ChatState.narrowSidebarExpanded ? (input.activeFocus && (!LlmClient.busy || ChatState.isWindow) ? 1 : 0) : 0
font: Tokens.font.body.small font: Tokens.font.body.small
implicitHeight: Math.min(root.height / 5, contentHeight + topPadding + bottomPadding) implicitHeight: Math.min(root.height / 5, contentHeight + topPadding + bottomPadding)
focus: true focus: true
@@ -578,4 +581,52 @@ Item {
root.send(text); root.send(text);
} }
} }
Connections {
target: DropImporter
function onImportFinished(id: int, paths: var, errors: var): void {
if (!root.importIds.includes(id))
return;
root.importIds = root.importIds.filter(i => i !== id);
if (paths.length > 0) {
const added = root.chatData.addAttachments(paths);
root.pendingAttachments = root.pendingAttachments.concat(added);
}
for (const e of errors)
console.warn("Drop import:", e);
}
}
DropArea {
id: dropArea
anchors.fill: parent
onEntered: drag => {
drag.accepted = drag.hasUrls || drag.hasText || drag.hasHtml || drag.formats.some(f => f.startsWith("image/"));
}
onDropped: drop => {
const formats = drop.formats;
const urls = drop.hasUrls ? drop.urls : [];
const text = drop.hasText ? drop.text : "";
const html = drop.hasHtml ? drop.html : "";
const imgFormat = formats.find(f => f.startsWith("image/"));
const raw = imgFormat ? drop.getDataAsArrayBuffer(imgFormat) : new ArrayBuffer(0);
switch (DropImporter.decide(urls, text, html, raw.byteLength > 0)) {
case DropImporter.Attach:
root.importIds = [...root.importIds, DropImporter.importAttachments(urls, html, raw)];
break;
case DropImporter.InsertText:
input.insert(input.cursorPosition, text || urls.map(u => u.toString()).join("\n"));
root.focusInput();
break;
}
drop.acceptProposedAction();
}
}
} }
+77 -25
View File
@@ -91,6 +91,50 @@ CustomRect {
opacity: root.slim ? 0 : 1 opacity: root.slim ? 0 : 1
state: titleText.readOnly ? "static" : "editing"
states: [
State {
name: "static"
PropertyChanges {
staticText.opacity: 1
}
PropertyChanges {
titleText.opacity: 0
}
},
State {
name: "editing"
PropertyChanges {
staticText.opacity: 0
}
PropertyChanges {
titleText.opacity: 1
}
}
]
transitions: [
// Going static: titleText fades out (animated), staticText snaps in
Transition {
to: "static"
Anim {
target: titleText
property: "opacity"
type: Anim.SlowEffects
}
},
// Going editing: staticText fades out (animated), titleText snaps in
Transition {
to: "editing"
Anim {
target: staticText
property: "opacity"
type: Anim.SlowEffects
}
}
]
Behavior on opacity { Behavior on opacity {
Anim { Anim {
type: Anim.DefaultEffects type: Anim.DefaultEffects
@@ -106,7 +150,7 @@ CustomRect {
color: Colors.layer(Colors.palette.m3surfaceContainerHighest, 1) color: Colors.layer(Colors.palette.m3surfaceContainerHighest, 1)
implicitHeight: titleText.implicitHeight + Tokens.padding.extraSmall * 2 implicitHeight: titleText.implicitHeight + Tokens.padding.extraSmall * 2
radius: Tokens.rounding.full radius: Tokens.rounding.full
implicitWidth: titleText.implicitWidth + Tokens.padding.large * 2 implicitWidth: titleText.width + Tokens.padding.large * 2
opacity: titleText.readOnly ? 0 : 1 opacity: titleText.readOnly ? 0 : 1
Behavior on opacity { Behavior on opacity {
@@ -122,11 +166,6 @@ CustomRect {
anchors.right: parent.right anchors.right: parent.right
implicitHeight: titleText.implicitHeight + Tokens.padding.small * 2 implicitHeight: titleText.implicitHeight + Tokens.padding.small * 2
layer.enabled: true
layer.effect: OpacityMask {
maskSource: fadeMask
}
TextFieldBase { TextFieldBase {
id: titleText id: titleText
@@ -134,10 +173,12 @@ CustomRect {
anchors.leftMargin: readOnly ? 0 : Tokens.padding.large anchors.leftMargin: readOnly ? 0 : Tokens.padding.large
anchors.top: parent.top anchors.top: parent.top
anchors.topMargin: readOnly ? Tokens.padding.small : Tokens.padding.extraSmall * 2 anchors.topMargin: readOnly ? Tokens.padding.small : Tokens.padding.extraSmall * 2
width: Math.min(implicitWidth, parent.width - Tokens.padding.large * 2)
font: Tokens.font.body.medium font: Tokens.font.body.medium
text: root.modelData.title || qsTr("New chat") text: root.modelData.title || qsTr("New chat")
readOnly: true readOnly: true
color: Colors.palette.m3onSurface color: Colors.palette.m3onSurface
opacity: 0
Behavior on anchors.leftMargin { Behavior on anchors.leftMargin {
Anim {} Anim {}
@@ -146,6 +187,11 @@ CustomRect {
Anim {} Anim {}
} }
function cancelTitleEdit(): void {
text = root.modelData.title;
readOnly = true;
}
onEditingFinished: root.modelData.title = text onEditingFinished: root.modelData.title = text
onReadOnlyChanged: { onReadOnlyChanged: {
if (!readOnly) { if (!readOnly) {
@@ -156,27 +202,24 @@ CustomRect {
} }
} }
} }
}
CustomRect { CustomText {
id: fadeMask id: staticText
anchors.fill: titleWrapper anchors.left: parent.left
anchors.leftMargin: titleText.readOnly ? 0 : Tokens.padding.large
anchors.top: parent.top
anchors.topMargin: titleText.readOnly ? Tokens.padding.small : Tokens.padding.extraSmall * 2
anchors.right: parent.right
font: titleText.font
text: titleText.text
elide: CustomText.ElideRight
layer.enabled: true Behavior on anchors.leftMargin {
visible: false Anim {}
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop {
color: Qt.rgba(1, 1, 1, 1.0)
position: 0.85
} }
Behavior on anchors.topMargin {
GradientStop { Anim {}
color: Qt.rgba(1, 1, 1, 0)
position: 1.0
} }
} }
} }
@@ -323,7 +366,13 @@ CustomRect {
inactiveColor: Colors.layer(Colors.palette.m3surfaceContainerHighest, 3) inactiveColor: Colors.layer(Colors.palette.m3surfaceContainerHighest, 3)
inactiveOnColor: Colors.palette.m3onSurfaceVariant inactiveOnColor: Colors.palette.m3onSurfaceVariant
onClicked: root.remove(root.modelData) onClicked: {
if (!titleText.readOnly) {
titleText.cancelTitleEdit();
return;
}
root.remove(root.modelData);
}
} }
IconButton { IconButton {
@@ -366,7 +415,10 @@ CustomRect {
} }
StateLayer { StateLayer {
onClicked: root.expanded = !root.expanded onClicked: {
root.expanded = !root.expanded;
titleText.readOnly = true;
}
} }
MaterialIcon { MaterialIcon {
+21 -3
View File
@@ -140,7 +140,7 @@ Item {
label.transform: Rotation { label.transform: Rotation {
origin.y: fabRoot.label.height / 2 origin.y: fabRoot.label.height / 2
origin.x: fabRoot.label.width / 2 origin.x: fabRoot.label.width / 2
angle: ChatState.fabExpanded && (ChatState.narrowSidebarExpanded && !ChatState.isWide || !ChatState.isWindow) ? 135 : 0 angle: ChatState.fabExpanded && (ChatState.narrowSidebarExpanded || !ChatState.isWide || !ChatState.isWindow) ? 135 : 0
Behavior on angle { Behavior on angle {
Anim {} Anim {}
@@ -148,8 +148,9 @@ Item {
} }
onClicked: { onClicked: {
if (root.slim && !ChatState.narrowSidebarExpanded) { if ((root.slim && !ChatState.narrowSidebarExpanded) || ChatState.shrinkSidebarOverride) {
root.requestExpand(); root.requestExpand();
ChatState.shrinkSidebarOverride = false;
return; return;
} }
@@ -183,6 +184,10 @@ Item {
name: "pop out in window" name: "pop out in window"
icon: "open_in_new" icon: "open_in_new"
} }
ListElement {
name: "shrink sidebar"
icon: "left_panel_close"
}
} }
CustomRect { CustomRect {
@@ -196,7 +201,18 @@ Item {
radius: sLayer.pressed ? Tokens.rounding.small : implicitHeight / 2 radius: sLayer.pressed ? Tokens.rounding.small : implicitHeight / 2
color: Colors.palette.m3primaryContainer color: Colors.palette.m3primaryContainer
visible: !(modelData.name === "pop out in window" && ChatState.isWindow) visible: {
const n = modelData.name;
const w = ChatState.isWindow;
const wide = ChatState.isWide;
if (n === "pop out in window" && w)
return false;
if (n === "shrink sidebar" && (!w || !wide))
return false;
else
return true;
}
clip: true clip: true
Behavior on radius { Behavior on radius {
@@ -297,6 +313,8 @@ Item {
root.newChatRequest(); root.newChatRequest();
else if (name === "pop out in window") else if (name === "pop out in window")
Detach.create(); Detach.create();
else if (name === "shrink sidebar")
ChatState.shrinkSidebarOverride = true;
ChatState.fabExpanded = false; ChatState.fabExpanded = false;
} }
+4 -1
View File
@@ -19,7 +19,7 @@ Singleton {
property bool settledSlim: false property bool settledSlim: false
property bool settledWide: false property bool settledWide: false
property bool narrowSidebarExpanded: false property bool narrowSidebarExpanded: false
property bool windowIsWide: false property bool shrinkSidebarOverride: false
property ShellScreen screen property ShellScreen screen
property bool sidebarVisible: ShellState.forActive().sidebar property bool sidebarVisible: ShellState.forActive().sidebar
@@ -28,6 +28,9 @@ Singleton {
root.currentIdx = -1; root.currentIdx = -1;
} }
onIsWideChanged: if (isWide)
narrowSidebarExpanded = false
onSidebarVisibleChanged: if (!sidebarVisible) onSidebarVisibleChanged: if (!sidebarVisible)
fabExpanded = false fabExpanded = false
} }
@@ -337,7 +337,7 @@ Item {
} }
} }
CustomClippingRect { Item {
id: expandedTool id: expandedTool
anchors.top: collapsedRow.bottom anchors.top: collapsedRow.bottom
@@ -348,8 +348,16 @@ 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
radius: Tokens.rounding.medium clip: true
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,7 +59,6 @@ Item {
id: container id: container
anchors.fill: parent anchors.fill: parent
layer.enabled: opacity < 1
objectName: "ConversationContainer" objectName: "ConversationContainer"
Component.onCompleted: { Component.onCompleted: {
@@ -220,9 +220,8 @@ CustomRect {
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
color: root.codeHeaderColor color: root.codeHeaderColor
font: Tokens.font.label.small font: Tokens.font.label.builders.small.capitalization(Font.Capitalize).build()
text: CodeIcons.name(root.language) text: CodeIcons.name(root.language)
visible: root.language.length > 0
} }
Item { Item {
@@ -252,7 +251,7 @@ CustomRect {
} }
} }
CustomClippingRect { CustomRect {
id: codeRect id: codeRect
anchors.left: parent.left anchors.left: parent.left
@@ -265,6 +264,7 @@ CustomRect {
implicitWidth: codeText.implicitWidth + codeFlick.anchors.margins * 2 implicitWidth: codeText.implicitWidth + codeFlick.anchors.margins * 2
implicitHeight: codeText.implicitHeight + codeFlick.anchors.margins * 2 implicitHeight: codeText.implicitHeight + codeFlick.anchors.margins * 2
color: CodeColors.active.bg color: CodeColors.active.bg
clip: true
Flickable { Flickable {
id: codeFlick id: codeFlick
@@ -272,6 +272,9 @@ CustomRect {
anchors.fill: parent anchors.fill: parent
anchors.margins: Tokens.padding.small anchors.margins: Tokens.padding.small
boundsBehavior: Flickable.StopAtBounds
contentWidth: codeText.implicitWidth
CustomScrollBar.horizontal: CustomScrollBar { CustomScrollBar.horizontal: CustomScrollBar {
flickable: codeFlick flickable: codeFlick
parent: codeFlick.parent parent: codeFlick.parent
@@ -280,16 +283,13 @@ CustomRect {
anchors.right: parent.right anchors.right: parent.right
} }
TextAreaBase.flickable: TextAreaBase { CustomText {
id: codeText id: codeText
color: CodeColors.active.normal color: CodeColors.active.normal
layer.enabled: true
clip: false
font: Tokens.font.mono.small font: Tokens.font.mono.small
textFormat: root.colored ? Text.RichText : Text.PlainText textFormat: Text.RichText
text: root.colored ? root.html : root.plainForDisplay(root.code) text: root.colored ? root.html : root.plainForDisplay(root.code)
readOnly: true
} }
} }
} }
File diff suppressed because one or more lines are too long
+11 -3
View File
@@ -125,7 +125,7 @@ Item {
current: root.current current: root.current
} }
CustomClippingRect { Item {
id: expandedRect id: expandedRect
anchors.left: parent.left anchors.left: parent.left
@@ -133,11 +133,19 @@ 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 {}
+9 -18
View File
@@ -21,12 +21,8 @@ Item {
property bool chatOpen: ChatState.chatSession !== null property bool chatOpen: ChatState.chatSession !== null
readonly property bool isWide: root.width >= root.breakpoint readonly property bool isWide: root.width >= root.breakpoint
readonly property bool wideExpandedSidebar: isWide && !ChatState.shrinkSidebarOverride
readonly property bool showList: isWide || !chatOpen readonly property bool narrowExpandedSidebar: !isWide && ChatState.narrowSidebarExpanded
readonly property bool showContent: !isWide && chatOpen
function openConversation(conv: ChatSession, index: int): void {
}
BlobGroup { BlobGroup {
id: blobGroup id: blobGroup
@@ -54,9 +50,9 @@ Item {
anchors.left: parent.left anchors.left: parent.left
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.margins: Tokens.padding.medium anchors.margins: Tokens.padding.medium
implicitWidth: root.isWide || ChatState.narrowSidebarExpanded ? Config.sidebar.sizes.width : root.iconSize implicitWidth: root.wideExpandedSidebar || root.narrowExpandedSidebar ? Config.sidebar.sizes.width : root.iconSize
highlight: true highlight: true
slim: !root.isWide && !ChatState.narrowSidebarExpanded slim: !root.wideExpandedSidebar && !root.narrowExpandedSidebar
z: 1 z: 1
Behavior on implicitWidth { Behavior on implicitWidth {
@@ -86,13 +82,13 @@ Item {
} }
} }
CustomClippingRect { Item {
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: invertedRect.borderLeft anchors.leftMargin: invertedRect.borderLeft
anchors.topMargin: invertedRect.borderTop anchors.topMargin: invertedRect.borderTop
anchors.bottomMargin: invertedRect.borderBottom anchors.bottomMargin: invertedRect.borderBottom
anchors.rightMargin: invertedRect.borderRight anchors.rightMargin: invertedRect.borderRight
radius: invertedRect.radius clip: true
children: [contentArea, dim] children: [contentArea, dim]
} }
@@ -100,9 +96,10 @@ Item {
CustomRect { CustomRect {
id: dim id: dim
anchors.fill: parent anchors.fill: parent
radius: invertedRect.radius
color: Colors.palette.m3shadow color: Colors.palette.m3shadow
opacity: ChatState.narrowSidebarExpanded && !root.isWide ? 0.3 : 0 opacity: root.narrowExpandedSidebar ? 0.3 : 0
z: 1 z: 1
Behavior on opacity { Behavior on opacity {
@@ -155,7 +152,7 @@ Item {
id: dimArea id: dimArea
anchors.fill: parent anchors.fill: parent
preventStealing: true preventStealing: true
enabled: !root.isWide && ChatState.narrowSidebarExpanded enabled: root.narrowExpandedSidebar
cursorShape: undefined cursorShape: undefined
onClicked: { onClicked: {
@@ -175,12 +172,6 @@ Item {
} }
} }
Binding {
target: ChatState
property: "windowIsWide"
value: root.showList
}
Binding { Binding {
target: ChatState target: ChatState
property: "isWide" property: "isWide"
-70
View File
@@ -17,76 +17,6 @@ Item {
Layout.fillWidth: true Layout.fillWidth: true
implicitHeight: flickable.contentHeight implicitHeight: flickable.contentHeight
layer.enabled: true
layer.smooth: true
layer.effect: Mask {
maskSource: gradientMask
}
Item {
id: gradientMask
anchors.fill: parent
layer.enabled: true
visible: false
Rectangle {
anchors.fill: parent
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop {
color: Qt.rgba(0, 0, 0, 0)
position: 0
}
GradientStop {
color: Qt.rgba(0, 0, 0, 1)
position: 0.1
}
GradientStop {
color: Qt.rgba(0, 0, 0, 1)
position: 0.9
}
GradientStop {
color: Qt.rgba(0, 0, 0, 0)
position: 1
}
}
}
Rectangle {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.top: parent.top
implicitWidth: parent.width / 2
opacity: flickable.contentX > 0 ? 0 : 1
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
}
Rectangle {
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.top: parent.top
implicitWidth: parent.width / 2
opacity: flickable.contentX < flickable.contentWidth - parent.width ? 0 : 1
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
}
}
CustomFlickable { CustomFlickable {
id: flickable id: flickable
+1 -1
View File
@@ -112,7 +112,7 @@ CustomRect {
} }
} }
CustomClippingRect { ClipRect {
anchors.fill: parent anchors.fill: parent
color: root.urgency === NotificationUrgency.Critical ? Colors.palette.m3error : root.urgency === NotificationUrgency.Low ? Colors.layer(Colors.palette.m3surfaceContainerHigh, 3) : Colors.palette.m3secondaryContainer color: root.urgency === NotificationUrgency.Critical ? Colors.palette.m3error : root.urgency === NotificationUrgency.Low ? Colors.layer(Colors.palette.m3surfaceContainerHigh, 3) : Colors.palette.m3secondaryContainer
radius: Tokens.rounding.full radius: Tokens.rounding.full
+8
View File
@@ -20,6 +20,14 @@ 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,6 +37,7 @@ 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)
+6 -1
View File
@@ -1,7 +1,10 @@
#pragma once #pragma once
#include "common.hpp"
#include <qqmlintegration.h> #include <qqmlintegration.h>
#include "common.hpp"
#include "enums.hpp"
namespace ZShell::config { namespace ZShell::config {
class Background : public settings::ObjectNode { class Background : public settings::ObjectNode {
@@ -9,6 +12,8 @@ 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,6 +23,19 @@ 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
+1 -1
View File
@@ -52,7 +52,7 @@ FontBuilder FontBuilder::letterSpacing(qreal spacing, bool absolute) {
return *this; return *this;
} }
FontBuilder FontBuilder::capitalisation(QFont::Capitalization cap) { FontBuilder FontBuilder::capitalization(QFont::Capitalization cap) {
m_font.setCapitalization(cap); m_font.setCapitalization(cap);
return *this; return *this;
} }
+1 -1
View File
@@ -23,7 +23,7 @@ class FontBuilder {
[[nodiscard]] Q_INVOKABLE FontBuilder [[nodiscard]] Q_INVOKABLE FontBuilder
letterSpacing(qreal spacing, bool absolute = true); letterSpacing(qreal spacing, bool absolute = true);
[[nodiscard]] Q_INVOKABLE FontBuilder [[nodiscard]] Q_INVOKABLE FontBuilder
capitalisation(QFont::Capitalization cap); capitalization(QFont::Capitalization cap);
[[nodiscard]] Q_INVOKABLE FontBuilder vaxis(const QString& tag, float value); [[nodiscard]] Q_INVOKABLE FontBuilder vaxis(const QString& tag, float value);
[[nodiscard]] Q_INVOKABLE FontBuilder vaxes(const QVariantMap& axes); [[nodiscard]] Q_INVOKABLE FontBuilder vaxes(const QVariantMap& axes);
[[nodiscard]] Q_INVOKABLE QFont build() const; [[nodiscard]] Q_INVOKABLE QFont build() const;
+20 -53
View File
@@ -14,9 +14,10 @@
#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/managers/EventManager.hpp> #include <hyprland/src/ipc/s2/S2.hpp>
extern "C" { extern "C" {
#include <lua.h> #include <lua.h>
@@ -27,6 +28,7 @@ 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;
@@ -149,25 +151,14 @@ static bool gainIsNeutral(const std::array<float, 3>& gain) {
} }
static CFunctionHook* g_pBeginHook = nullptr; static CFunctionHook* g_pBeginHook = nullptr;
using origBegin = void (*)( using origBegin = bool (*)(
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;
@@ -198,34 +189,33 @@ 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 void hkBegin( static bool 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) {
(*reinterpret_cast<origBegin>( const bool ok = (*reinterpret_cast<origBegin>(
g_pBeginHook->m_original))(thisptr, mon, damage, fb, finalDamage); g_pBeginHook->m_original))(thisptr, ctx, mon, damage, fb, finalDamage);
if (!ok) return false;
if (fb) { if (fb) ctx.m_data.blockScreenShader = true;
auto* renderer = g_pHyprRenderer.get();
if (renderer) renderer->m_renderData.blockScreenShader = true;
}
if (!g_active) return; if (!g_active) return true;
auto* gl = g_pHyprOpenGL.get(); auto* gl = g_pHyprOpenGL.get();
if (!gl) return; if (!gl) return true;
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; return true;
if (g_transition.active) { if (g_transition.active) {
auto* monPtr = mon.get(); auto* monPtr = mon.get();
if (!monPtr) return; if (!monPtr) return true;
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)
@@ -265,6 +255,7 @@ static void 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;
@@ -333,9 +324,10 @@ static int luaNightlightState(lua_State* L) {
} }
static void postStateEvent(bool enabled, bool animating) { static void postStateEvent(bool enabled, bool animating) {
if (!g_pEventManager) return; auto& sock = IPC::Socket2::sock();
g_pEventManager->postEvent( if (!sock) return;
SHyprIPCEvent{ sock->postEvent(
IPC::Socket2::SEvent{
.event = "zshell-nightlight", .event = "zshell-nightlight",
.data = std::format( .data = std::format(
"{},{},{},{}", "{},{},{},{}",
@@ -377,31 +369,6 @@ 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 =
+2 -13
View File
@@ -1,8 +1,3 @@
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
@@ -13,10 +8,6 @@ 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
@@ -27,16 +18,14 @@ 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
) )
@@ -1,743 +0,0 @@
#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;
}
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";
}
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
@@ -1,147 +0,0 @@
#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

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