106 Commits
Author SHA1 Message Date
zach d3345906b9 Merge pull request 'Feat: LLM Chat' (#155) from feat/llm-chat-tab into main
Reviewed-on: #155
2026-10-18 17:05:05 +02:00
zach 08d42e1f8a chore: formatter & clangd lsp option
C++ / fmt (pull_request) Successful in 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 27s
Rust / build (pull_request) Successful in 50s
Python / verify (pull_request) Successful in 1m40s
Rust / fmt (pull_request) Successful in 28s
Rust / clippy (pull_request) Successful in 45s
2026-10-18 15:23:40 +02:00
zach 45aa6c9005 Merge branch 'main' into feat/llm-chat-tab
C++ / fmt (pull_request) Failing after 6s
C++ / build (pull_request) Failing after 20s
C++ / clang-tidy (pull_request) Failing after 12s
JS/TS / fmt (pull_request) Successful in 6s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Successful in 28s
Rust / build (pull_request) Successful in 48s
Rust / fmt (pull_request) Successful in 28s
Python / verify (pull_request) Successful in 1m45s
Rust / clippy (pull_request) Successful in 46s
2026-10-18 14:43:00 +02:00
zach 1763b03287 Merge pull request 'Chore/remove nix support' (#159) from chore/remove-nix-support into main
Reviewed-on: #159
2026-10-18 14:42:50 +02:00
zach 72ff6ad42a Merge branch 'main' into chore/remove-nix-support
C++ / fmt (pull_request) Failing after 4s
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) Successful in 28s
Rust / build (pull_request) Successful in 50s
Python / verify (pull_request) Successful in 1m35s
Rust / fmt (pull_request) Successful in 29s
Rust / clippy (pull_request) Successful in 55s
2026-10-18 14:42:43 +02:00
zach 2a45740020 Merge pull request 'fix(pr-template): adds an "Other" issue type' (#160) from fix/ci-pr-template into main
Reviewed-on: #160
2026-10-18 14:42:35 +02:00
zach 0a503bc27c fix: remove unimplemented method
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 8s
Python / static (pull_request) Successful in 27s
Rust / build (pull_request) Successful in 49s
Rust / fmt (pull_request) Successful in 28s
Python / verify (pull_request) Successful in 1m41s
Rust / clippy (pull_request) Successful in 45s
2026-10-18 13:03:25 +02:00
zach 1fe8a3a959 chore: restructure llm plugin to be more coherent & fix various bugs in llm ui
C++ / fmt (pull_request) Failing after 6s
C++ / build (pull_request) Failing after 10s
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) Successful in 30s
Rust / build (pull_request) Successful in 49s
Python / verify (pull_request) Successful in 1m35s
Rust / fmt (pull_request) Successful in 36s
Rust / clippy (pull_request) Successful in 54s
2026-10-18 12:58:38 +02:00
AramJonghu ae25f33fc8 fix(pr-template): adds an "Other" issue type
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 10s
Rust / fmt (pull_request) Successful in 48s
Rust / build (pull_request) Successful in 1m9s
Rust / clippy (pull_request) Successful in 52s
Python / static (pull_request) Successful in 55s
JS/TS / fmt (pull_request) Successful in 11s
JS/TS / lint (pull_request) Successful in 9s
Python / verify (pull_request) Successful in 1m26s
2026-10-14 00:22:33 +02:00
AramJonghu 02267ec9a6 docs(readme): adjust Nix support documentation
JS/TS / fmt (pull_request) Successful in 7s
JS/TS / lint (pull_request) Successful in 8s
Python / static (pull_request) Successful in 27s
Rust / build (pull_request) Successful in 50s
Rust / fmt (pull_request) Successful in 32s
Python / verify (pull_request) Successful in 2m16s
C++ / fmt (pull_request) Failing after 7s
C++ / build (pull_request) Failing after 12s
Rust / clippy (pull_request) Successful in 50s
C++ / clang-tidy (pull_request) Failing after 26s
2026-10-14 00:08:17 +02:00
AramJonghu 4b8d74a7e5 ci(cleanup): removed NixOS from issue template
Avoids confusion
2026-10-14 00:08:17 +02:00
AramJonghu 1fa3c84a56 chore(nix): remove all .nix files and flake.lock 2026-10-14 00:08:17 +02:00
zach fc81be922f Merge branch 'main' into feat/llm-chat-tab
C++ / fmt (pull_request) Failing after 6s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 9s
JS/TS / fmt (pull_request) Successful in 7s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Successful in 28s
Rust / build (pull_request) Successful in 48s
Python / verify (pull_request) Successful in 1m38s
Rust / fmt (pull_request) Successful in 28s
Rust / clippy (pull_request) Successful in 46s
2026-10-12 16:06:23 +02:00
zach 8d54e1ce6f fix: undo accidental debug code 2026-10-12 16:05:52 +02:00
zach bee8cd061b Merge branch 'main' into feat/llm-chat-tab
C++ / fmt (pull_request) Failing after 7s
C++ / build (pull_request) Failing after 11s
C++ / clang-tidy (pull_request) Failing after 11s
JS/TS / fmt (pull_request) Successful in 10s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Successful in 28s
Rust / build (pull_request) Successful in 48s
Python / verify (pull_request) Successful in 1m39s
Rust / fmt (pull_request) Successful in 28s
Rust / clippy (pull_request) Successful in 46s
2026-10-12 15:58:33 +02:00
zach b9e818faa9 fix: add proper atRestOverride for notification background 2026-10-12 15:58:25 +02:00
zach 2cabd72fc0 Merge branch 'main' into feat/llm-chat-tab
C++ / fmt (pull_request) Failing after 4s
C++ / build (pull_request) Failing after 12s
C++ / clang-tidy (pull_request) Failing after 12s
JS/TS / fmt (pull_request) Successful in 8s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Successful in 31s
Rust / build (pull_request) Successful in 49s
Python / verify (pull_request) Successful in 1m32s
Rust / fmt (pull_request) Successful in 51s
Rust / clippy (pull_request) Successful in 1m1s
2026-10-12 15:54:52 +02:00
zach 255db9e533 fix: ignore stale coordinate data for panels + convert wavyline to use new qt 6.12 canvas painter api 2026-10-12 15:54:32 +02:00
zach a4f6aa1d4a Merge branch 'main' into feat/llm-chat-tab
C++ / fmt (pull_request) Failing after 6s
C++ / build (pull_request) Failing after 16s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 14s
JS/TS / lint (pull_request) Successful in 8s
Python / static (pull_request) Successful in 29s
Rust / build (pull_request) Successful in 50s
Python / verify (pull_request) Successful in 1m37s
Rust / fmt (pull_request) Successful in 31s
Rust / clippy (pull_request) Successful in 49s
2026-10-11 08:42:22 +02:00
zach 3883917228 fix: ui no longer blocks when volumes are unreachable + add support for network and fuse volumes 2026-10-11 08:42:05 +02:00
zach bf6a7027e4 Merge branch 'main' into feat/llm-chat-tab
C++ / fmt (pull_request) Failing after 7s
C++ / build (pull_request) Failing after 13s
C++ / clang-tidy (pull_request) Failing after 12s
JS/TS / fmt (pull_request) Successful in 9s
JS/TS / lint (pull_request) Successful in 8s
Python / static (pull_request) Successful in 27s
Rust / build (pull_request) Successful in 49s
Rust / fmt (pull_request) Successful in 28s
Python / verify (pull_request) Successful in 1m40s
Rust / clippy (pull_request) Successful in 47s
2026-10-08 12:09:59 +02:00
zach 873b2eb01b fix: dashboard tabs scroll now only loads previous item + next item 2026-10-08 12:09:50 +02:00
zach d70fb61e4a gemini glow fixes + positioning for window view
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 9s
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) Successful in 28s
Rust / build (pull_request) Successful in 50s
Python / verify (pull_request) Successful in 1m38s
Rust / fmt (pull_request) Successful in 27s
Rust / clippy (pull_request) Successful in 53s
2026-10-08 12:08:48 +02:00
zach 4c410d638c Merge branch 'main' into feat/llm-chat-tab
C++ / fmt (pull_request) Failing after 7s
C++ / build (pull_request) Failing after 21s
C++ / clang-tidy (pull_request) Failing after 12s
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 47s
Python / verify (pull_request) Successful in 1m33s
Rust / fmt (pull_request) Successful in 28s
Rust / clippy (pull_request) Successful in 49s
2026-10-07 22:39:19 +02:00
zach aef7cd5918 fix: qt 6.12 build 2026-10-07 22:38:49 +02:00
zach a13fa8442b fix: qt 6.12 build
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 22s
JS/TS / fmt (pull_request) Successful in 10s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Successful in 28s
Rust / build (pull_request) Successful in 54s
Python / verify (pull_request) Successful in 1m41s
Rust / fmt (pull_request) Successful in 28s
Rust / clippy (pull_request) Successful in 53s
2026-10-07 22:37:46 +02:00
zach 4d75b317cd revert rounding var
C++ / fmt (pull_request) Failing after 6s
C++ / build (pull_request) Failing after 18s
C++ / clang-tidy (pull_request) Failing after 11s
JS/TS / fmt (pull_request) Successful in 8s
JS/TS / lint (pull_request) Successful in 12s
Python / static (pull_request) Successful in 30s
Rust / build (pull_request) Successful in 52s
Python / verify (pull_request) Successful in 1m38s
Rust / fmt (pull_request) Successful in 31s
Rust / clippy (pull_request) Successful in 1m0s
2026-10-07 13:33:54 +02:00
zach 76823eff79 Merge branch 'main' into feat/llm-chat-tab
C++ / fmt (pull_request) Failing after 29s
C++ / build (pull_request) Failing after 33s
C++ / clang-tidy (pull_request) Failing after 11s
JS/TS / fmt (pull_request) Successful in 11s
JS/TS / lint (pull_request) Successful in 8s
Python / static (pull_request) Successful in 28s
Rust / build (pull_request) Successful in 51s
Python / verify (pull_request) Successful in 1m36s
Rust / fmt (pull_request) Successful in 43s
Rust / clippy (pull_request) Successful in 1m4s
2026-10-07 13:32:49 +02:00
zach 909b5113fb fix: closed panels don't add resistance to bounce + use qtrendering for text 2026-10-07 13:32:37 +02:00
zach c06cbea854 Merge branch 'main' into feat/llm-chat-tab
C++ / fmt (pull_request) Failing after 4s
C++ / build (pull_request) Failing after 9s
C++ / clang-tidy (pull_request) Failing after 11s
JS/TS / fmt (pull_request) Successful in 8s
JS/TS / lint (pull_request) Successful in 8s
Python / static (pull_request) Successful in 30s
Rust / build (pull_request) Successful in 48s
Python / verify (pull_request) Successful in 1m40s
Rust / fmt (pull_request) Successful in 25s
Rust / clippy (pull_request) Successful in 43s
2026-10-06 13:33:41 +02:00
zach 376595cd0b fix: don't use zoom value when rendering wallpaper 2026-10-06 13:30:27 +02:00
zach a00bbe50f2 add rounding var
C++ / fmt (pull_request) Failing after 4s
C++ / build (pull_request) Failing after 9s
C++ / clang-tidy (pull_request) Failing after 9s
JS/TS / fmt (pull_request) Successful in 8s
JS/TS / lint (pull_request) Successful in 8s
Python / static (pull_request) Successful in 28s
Rust / build (pull_request) Successful in 48s
Python / verify (pull_request) Successful in 1m43s
Rust / fmt (pull_request) Successful in 25s
Rust / clippy (pull_request) Successful in 46s
2026-10-06 00:55:50 +02:00
zach 11e94fbb4b Merge branch 'main' into feat/llm-chat-tab
C++ / fmt (pull_request) Failing after 4s
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) Successful in 28s
Rust / build (pull_request) Successful in 47s
Python / verify (pull_request) Successful in 1m40s
Rust / fmt (pull_request) Successful in 29s
Rust / clippy (pull_request) Successful in 46s
2026-10-05 19:33:42 +02:00
zach 9f5e62f9c7 feat: use custom wayland layer-shell to render wallpaper to reduce vram 2026-10-05 19:31:15 +02:00
zach b91befa9f7 Merge branch 'main' into feat/llm-chat-tab
C++ / fmt (pull_request) Failing after 4s
C++ / build (pull_request) Failing after 9s
C++ / clang-tidy (pull_request) Failing after 9s
JS/TS / fmt (pull_request) Successful in 7s
JS/TS / lint (pull_request) Successful in 11s
Python / static (pull_request) Successful in 30s
Rust / build (pull_request) Successful in 49s
Python / verify (pull_request) Successful in 1m30s
Rust / fmt (pull_request) Successful in 30s
Rust / clippy (pull_request) Successful in 48s
2026-10-05 09:58:00 +02:00
zach f90e9bb1a0 fix: all apps icon size and suppress warnings 2026-10-05 09:57:47 +02:00
zach 99fdfbc465 Merge branch 'main' into feat/llm-chat-tab
C++ / fmt (pull_request) Failing after 4s
C++ / clang-tidy (pull_request) Failing after 10s
C++ / build (pull_request) Failing after 9s
JS/TS / fmt (pull_request) Successful in 9s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Successful in 35s
Rust / build (pull_request) Successful in 50s
Python / verify (pull_request) Successful in 1m40s
Rust / fmt (pull_request) Successful in 28s
Rust / clippy (pull_request) Successful in 51s
2026-10-04 18:20:23 +02:00
zach 2c88b7abcb move picker to main panelwindow to reduce vram usage 2026-10-04 18:20:10 +02:00
zach e148a0f843 Merge branch 'main' into feat/llm-chat-tab
C++ / fmt (pull_request) Failing after 6s
C++ / build (pull_request) Failing after 12s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 8s
JS/TS / lint (pull_request) Successful in 8s
Python / static (pull_request) Successful in 26s
Rust / build (pull_request) Successful in 48s
Rust / fmt (pull_request) Successful in 29s
Python / verify (pull_request) Successful in 1m40s
Rust / clippy (pull_request) Successful in 44s
2026-10-04 13:55:22 +02:00
zach f1918172c3 fix: merge two strokecanvas into one + better cursorShape and pointer handling
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 9s
C++ / clang-tidy (pull_request) Failing after 9s
JS/TS / fmt (pull_request) Successful in 8s
JS/TS / lint (pull_request) Successful in 8s
Python / static (pull_request) Successful in 28s
Rust / build (pull_request) Successful in 47s
Python / verify (pull_request) Successful in 1m34s
Rust / fmt (pull_request) Successful in 35s
Rust / clippy (pull_request) Successful in 1m0s
2026-10-04 13:53:21 +02:00
zach 5507b99fdc fix: merge two strokecanvas into one + better cursorShape and pointer handling 2026-10-04 13:48:50 +02:00
zach 506b69165d Merge branch 'main' into feat/llm-chat-tab
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 16s
JS/TS / lint (pull_request) Successful in 10s
Python / static (pull_request) Successful in 35s
Rust / build (pull_request) Successful in 50s
Python / verify (pull_request) Successful in 1m39s
Rust / fmt (pull_request) Successful in 29s
Rust / clippy (pull_request) Successful in 51s
2026-10-03 20:26:19 +02:00
zach dc99d6677c remove layer usage to reduce vram consumption 2026-10-03 20:26:00 +02:00
zach add3beed59 Merge branch 'main' into feat/llm-chat-tab
C++ / fmt (pull_request) Failing after 4s
C++ / build (pull_request) Failing after 9s
C++ / clang-tidy (pull_request) Failing after 9s
JS/TS / fmt (pull_request) Successful in 9s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Successful in 30s
Rust / build (pull_request) Successful in 48s
Python / verify (pull_request) Successful in 1m37s
Rust / fmt (pull_request) Successful in 26s
Rust / clippy (pull_request) Successful in 47s
2026-10-02 19:54:45 +02:00
zach fc672924d1 fix: settings gpu enum 2026-10-02 19:54:33 +02:00
zach 4b67cc1d88 Merge branch 'main' into feat/llm-chat-tab
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 8s
C++ / clang-tidy (pull_request) Failing after 9s
JS/TS / fmt (pull_request) Successful in 10s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Successful in 32s
Rust / build (pull_request) Successful in 51s
Rust / fmt (pull_request) Successful in 29s
Python / verify (pull_request) Successful in 1m55s
Rust / clippy (pull_request) Successful in 45s
2026-10-01 17:11:29 +02:00
zach 765b84a19c fix: settings drag gesture and dashboard popout regions 2026-10-01 17:11:20 +02:00
zach c64088441b Merge branch 'main' into feat/llm-chat-tab
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 24s
C++ / clang-tidy (pull_request) Failing after 18s
JS/TS / fmt (pull_request) Successful in 9s
JS/TS / lint (pull_request) Successful in 8s
Python / static (pull_request) Successful in 27s
Rust / build (pull_request) Successful in 52s
Rust / fmt (pull_request) Successful in 29s
Python / verify (pull_request) Successful in 1m44s
Rust / clippy (pull_request) Successful in 45s
2026-09-30 23:33:42 +02:00
zach ad36b4ed8f fix: sizing and positioning of portrait aspect ratios for wallpaper cropper 2026-09-30 23:33:22 +02:00
zach e8bf8b9727 fix: restore lost changes
C++ / fmt (pull_request) Failing after 3s
C++ / build (pull_request) Failing after 9s
C++ / clang-tidy (pull_request) Failing after 9s
JS/TS / fmt (pull_request) Successful in 7s
JS/TS / lint (pull_request) Successful in 8s
Python / static (pull_request) Successful in 25s
Rust / build (pull_request) Successful in 50s
Rust / fmt (pull_request) Successful in 30s
Python / verify (pull_request) Successful in 1m44s
Rust / clippy (pull_request) Successful in 45s
2026-09-30 19:30:06 +02:00
zach 54f966b4ea Merge branch 'main' into feat/llm-chat-tab
C++ / fmt (pull_request) Failing after 4s
C++ / build (pull_request) Failing after 9s
C++ / clang-tidy (pull_request) Failing after 9s
JS/TS / fmt (pull_request) Successful in 6s
JS/TS / lint (pull_request) Successful in 8s
Python / static (pull_request) Successful in 29s
Rust / build (pull_request) Successful in 47s
Rust / fmt (pull_request) Successful in 30s
Python / verify (pull_request) Successful in 1m43s
Rust / clippy (pull_request) Successful in 46s
2026-09-30 18:41:12 +02:00
zach c1f26e7dcb fix: add wheelinverter to cmake 2026-09-30 18:40:46 +02:00
zach 8dd65a90a8 fix: re-add lost changes missed files 2026-09-30 18:39:24 +02:00
zach 3c359ce25f fix: re-add lost changes 2026-09-30 18:38:07 +02:00
zach 63b96471b0 Merge branch 'main' into feat/llm-chat-tab
C++ / fmt (pull_request) Failing after 3s
C++ / build (pull_request) Failing after 9s
C++ / clang-tidy (pull_request) Failing after 9s
JS/TS / fmt (pull_request) Successful in 9s
JS/TS / lint (pull_request) Successful in 10s
Python / static (pull_request) Successful in 28s
Rust / build (pull_request) Successful in 49s
Python / verify (pull_request) Successful in 1m40s
Rust / fmt (pull_request) Successful in 27s
Rust / clippy (pull_request) Successful in 46s
2026-09-30 17:59:14 +02:00
zach 42026ba5e5 Merge pull request 'Feat: Dashboard tabs and repositioning. New processes view. Settings backend changes' (#158) from feat/dashboard-tabs-processes-and-settings-backend into main
Reviewed-on: #158
2026-09-30 16:51:15 +02:00
zach e86d1e8220 remove unrelated files
C++ / fmt (pull_request) Failing after 4s
C++ / build (pull_request) Failing after 13s
C++ / clang-tidy (pull_request) Failing after 18s
JS/TS / fmt (pull_request) Successful in 6s
JS/TS / lint (pull_request) Successful in 8s
Python / static (pull_request) Successful in 24s
Rust / build (pull_request) Successful in 54s
Rust / fmt (pull_request) Successful in 32s
Python / verify (pull_request) Successful in 1m45s
Rust / clippy (pull_request) Successful in 46s
2026-09-30 15:18:16 +02:00
zach 4f797a9741 separate dashboard and settings changes to new branch
C++ / fmt (pull_request) Successful in 6s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 9s
JS/TS / fmt (pull_request) Successful in 11s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Successful in 28s
Rust / build (pull_request) Successful in 49s
Python / verify (pull_request) Successful in 1m40s
Rust / fmt (pull_request) Successful in 26s
Rust / clippy (pull_request) Successful in 46s
2026-09-30 15:11:51 +02:00
zach 83bc153ee3 fix: format and apply lint fixes in python script 2026-09-30 15:01:28 +02:00
zach ca51f660ad chore: format & fix prettierignore 2026-09-30 15:01:28 +02:00
zach 5153cc4f17 Merge branch 'main' into feat/llm-chat-tab 2026-09-30 15:01:28 +02:00
Inorishioandzach bdfb9fc241 fixed: mousewheel swallow 2026-09-30 15:01:28 +02:00
zach 13f1a3fede chore: format 2026-09-30 15:01:28 +02:00
zach d8a3d5732e new chatlistview component, file attachments, performance & primitive test 2026-09-30 15:01:28 +02:00
zach 51ed270774 Merge branch 'main' into feat/llm-chat-tab 2026-09-30 15:01:28 +02:00
zach 83f6f95f6e gitignore 2026-09-30 15:01:28 +02:00
zach cafdce2352 Merge branch 'main' into feat/llm-chat-tab 2026-09-30 15:01:28 +02:00
zach 5fc6fda804 fix: truncate context logic 2026-09-30 15:01:28 +02:00
zach a59e4286ad fix: tool call prompt 2026-09-30 15:01:28 +02:00
zach e2786a3f1e fix: truncate webfetch content to fit context 2026-09-30 15:01:28 +02:00
zach e700a4f96b use blobgroup for window chat interface + slim sidebar 2026-09-30 15:01:28 +02:00
zach a8fdc31b8d feat: implement findChild* methods 2026-09-30 15:01:28 +02:00
zach bc927b16af feat: implement findChild* methods 2026-09-30 15:01:28 +02:00
zach 57a6e48dcb fix: property link modules 2026-09-30 15:01:28 +02:00
zach 15769ba78e fix: add gnuinstalldirs include 2026-09-30 15:01:28 +02:00
zach be541d8f8d fix: cmake install dir 2026-09-30 15:01:28 +02:00
zach 0d4c08cfbb fix: cmake install dir 2026-09-30 15:01:28 +02:00
zach d3ac675492 fix: uncomment settings index cache path 2026-09-30 15:01:28 +02:00
zach e58440fea4 chore: split cmake into multiple files and fix settings searching regex + add enabled option to llm 2026-09-30 15:01:28 +02:00
zach d5d2311c24 fix: font family 2026-09-30 15:01:19 +02:00
AramJonghuandzach 5ec682d780 fix(Docker): add required deps to dockerfile 2026-09-30 15:01:19 +02:00
zach 79bedd5119 chore: format python codeicons 2026-09-30 15:01:19 +02:00
zach 14b54206b1 chore: format llm c files 2026-09-30 15:01:19 +02:00
zach 2df1cfe9aa chore: format llm c files 2026-09-30 15:01:19 +02:00
zach 88bb413f9a fab menu and model selector 2026-09-30 15:01:19 +02:00
zach 1ab49d4b38 better support for opening chat in floating window 2026-09-30 15:01:19 +02:00
zach 7cc673430c separate dashboard and settings changes to new branch
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 5s
JS/TS / lint (pull_request) Successful in 7s
Python / static (pull_request) Successful in 26s
Rust / build (pull_request) Successful in 46s
Python / verify (pull_request) Successful in 1m38s
Rust / fmt (pull_request) Successful in 28s
Rust / clippy (pull_request) Successful in 49s
2026-09-30 14:48:38 +02:00
zach 08bb221699 feat: config object migration path and add new options to settings 2026-09-30 14:40:22 +02:00
zach 60293abe61 fix: define declared method + insert new bar entries to config file list + rename Hyprsunset -> Nightlight 2026-09-30 14:33:58 +02:00
zach a22022e969 fix: changesMade bool now binding for robustness 2026-09-30 14:26:59 +02:00
zach 62e19b5261 fix: show bar on hover 2026-09-30 14:26:59 +02:00
zach d76aaf093b fix: normalize zoom slider to linear scale 2026-09-30 14:26:59 +02:00
zach 4ebb4ce859 feat: wallpaper cropper sdf shader rectangle, better confirmation controls + hide passive tray icons 2026-09-30 14:26:59 +02:00
zach be3c938c39 feat: progress on changing interactions to mousearea again 2026-09-30 14:26:59 +02:00
zach f6fa2f0230 fix: remove clippingrectangle in favor of passing radius to sub-items 2026-09-30 14:26:59 +02:00
zach 4a021d81ac chore: formatter 2026-09-30 14:26:59 +02:00
zach 10d763cfe5 fix: define declared method + insert new bar entries to config file list + rename Hyprsunset -> Nightlight 2026-09-30 14:26:56 +02:00
zach 133d453f6f fix: fix nvidia update interval on config change + more robust read path for generic 2026-09-30 14:26:50 +02:00
zach 3373f11193 fix: column index desync after reorder 2026-09-30 14:26:50 +02:00
zach 359f832609 more unfinished changes, process tab for dashboard 2026-09-30 14:26:50 +02:00
zach 0bfda04e91 intermediary changes, plan to revert to mousearea for gestures + config load bugfixes 2026-09-30 14:23:46 +02:00
zach f825e4d53b feat: fontbuilder to streamline font styling 2026-09-30 14:23:31 +02:00
zach 2c90339c77 feat: dashboard show on hover, media and weather tabs added 2026-09-30 14:22:22 +02:00
zach d259c6ffbe fix: shader fade animation linear curve 2026-09-30 14:17:16 +02:00
zach 5330ae71d2 add nightlight hyprland plugin + rename hyprsunsetmanager -> nightlightmanager to control both 2026-09-30 14:17:16 +02:00
zach 23d2b31c0a expandable dialogs for color scheme, fonts and scheduling modes 2026-09-30 14:16:16 +02:00
161 changed files with 7431 additions and 3966 deletions
+5
View File
@@ -0,0 +1,5 @@
CompileFlags:
Remove:
# GCC-only flags from the system's default spec that clang/clangd cannot
# parse. They appear in compile_commands.json but are not set by the project.
- -mno-direct-extern-access
-1
View File
@@ -38,7 +38,6 @@ body:
attributes:
label: OS
options:
- NixOS
- Arch
- Debian / Ubuntu
- Fedora
+1
View File
@@ -30,6 +30,7 @@ body:
- Refactor / style
- CI / build / tooling
- Documentation
- Other (Describe in description box with header "Issue type")
validations:
required: true
+11 -2
View File
@@ -61,10 +61,12 @@ set(CMAKE_INSTALL_MESSAGE NEVER)
add_compile_options(
-Wall -Wextra -Wpedantic -Wshadow -Wconversion
-Wold-style-cast -Wnull-dereference -Wdouble-promotion
-Wformat=2 -Wfloat-equal -Woverloaded-virtual
-Wnull-dereference -Wdouble-promotion
-Wformat=2 -Wfloat-equal
-Wsign-conversion -Wredundant-decls -Wswitch
-Wunreachable-code
$<$<COMPILE_LANGUAGE:CXX>:-Wold-style-cast>
$<$<COMPILE_LANGUAGE:CXX>:-Woverloaded-virtual>
)
@@ -188,4 +190,11 @@ if("m3shapes" IN_LIST ENABLE_MODULES)
if(TARGET m3shapesplugin)
set_target_properties(m3shapesplugin PROPERTIES INSTALL_RPATH "$ORIGIN")
endif()
# Silence warnings that come from m3shapes files we do not manage.
foreach(m3shapes_target m3shapes_core m3shapes_morph m3shapes_shapes m3shapes m3shapesplugin)
if(TARGET ${m3shapes_target})
target_compile_options(${m3shapes_target} PRIVATE -Wno-shadow -Wno-float-equal)
endif()
endforeach()
endif()
+1 -1
View File
@@ -20,7 +20,7 @@ BusyIndicator {
property int animState
property color bgColor: Colors.palette.m3secondaryContainer
property color fgColor: Colors.palette.m3primary
property real implicitSize: Tokens.font.size.normal * 3
property real implicitSize: Tokens.font.body.medium.pointSize * 3
property real internalStrokeWidth: strokeWidth
readonly property alias progress: manager.progress
property real strokeWidth: Tokens.padding.extraSmall * 0.8
+1
View File
@@ -30,6 +30,7 @@ Flickable {
onTriggered: {
root.doneFakeFlick = false;
root.returnToBounds();
}
}
}
+1
View File
@@ -27,6 +27,7 @@ ScrollBar {
property bool inverted
enabled: !ShellState.forActive().isDrawing
implicitWidth: size === 1 ? 0 : isHorizontal ? 0 : Tokens.padding.extraSmall * 2
implicitHeight: size === 1 ? 0 : isHorizontal ? Tokens.padding.extraSmall * 2 : 0
+5 -3
View File
@@ -16,11 +16,12 @@ Text {
color: Colors.palette.m3onSurface
font: Tokens.font.body.small
linkColor: Colors.palette.m3onPrimaryFixedVariant
renderType: Text.NativeRendering
renderType: Text.QtRendering
textFormat: Text.PlainText
Behavior on color {
CAnim {}
CAnim {
}
}
Behavior on text {
enabled: root.animate
@@ -32,7 +33,8 @@ Text {
type: Anim.FastEffects
}
PropertyAction {}
PropertyAction {
}
TAnim {
target: root
Executable → Regular
View File
+1
View File
@@ -9,5 +9,6 @@ CustomText {
property int grade: Colors.light ? 0 : -25
property font fontStyle: Tokens.font.icon.small
renderType: Text.NativeRendering
font: Tokens.font.icon.size(fontStyle.pointSize).weight(fontStyle.weight).vaxes(fontStyle.variableAxes).fill(fill.toFixed(1)).grade(grade).build()
}
+13 -12
View File
@@ -124,7 +124,7 @@ MouseArea {
model: root.items
CustomRect {
id: item
id: menuItem
readonly property bool active: modelData === root?.active
required property int index
@@ -142,13 +142,13 @@ MouseArea {
}
StateLayer {
color: item.active ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurface
color: menuItem.active ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurface
enabled: root.expanded
onClicked: {
root.itemSelected(item.modelData);
root.active = item.modelData;
item.modelData.clicked();
root.itemSelected(menuItem.modelData);
root.active = menuItem.modelData;
menuItem.modelData.clicked();
root.expanded = false;
}
}
@@ -162,26 +162,27 @@ MouseArea {
MaterialIcon {
Layout.alignment: Qt.AlignVCenter
color: item.active ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurfaceVariant
text: item.modelData?.icon ?? ""
color: menuItem.active ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurfaceVariant
text: menuItem.modelData?.icon ?? ""
visible: text !== ""
}
CustomText {
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
color: item.active ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurface
text: item.modelData?.text ?? ""
color: menuItem.active ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurface
text: menuItem.modelData?.text ?? ""
}
Loader {
Layout.alignment: Qt.AlignVCenter
active: item.modelData?.trailingIcon.length > 0
active: menuItem.modelData?.trailingIcon.length > 0
asynchronous: true
visible: active
sourceComponent: MaterialIcon {
color: item.active ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurfaceVariant
text: item.modelData.trailingIcon
color: menuItem.active ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurfaceVariant
text: menuItem.modelData.trailingIcon
}
}
}
+5 -2
View File
@@ -19,11 +19,11 @@ PersistentProperties {
property bool sidebar
// Dashboard state
property int dashboardTab
property int dashboardTab: 0
property date dashboardDate: new Date()
// Sidebar state
property int sidebarTab
property int sidebarTab: 0
property list<string> expandedNotifs: []
// Utilities state
@@ -34,4 +34,7 @@ PersistentProperties {
// Drawing state
property color penColor: "white"
property int penWidth: 4
// Screenshot state
property bool screenshot
}
+36 -43
View File
@@ -2,62 +2,55 @@ import QtQuick
import ZShell.Config
import qs.Services
Item {
CustomClippingRect {
id: track
required property var scrollBar
required property CustomMouseArea mouseArea
required property var mouseArea
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.top: parent.top
implicitWidth: Tokens.padding.extraSmall * 2
implicitWidth: handle.implicitWidth
radius: Tokens.rounding.full
CustomRect {
id: handle
CustomClippingRect {
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.top: parent.top
radius: Tokens.rounding.full
implicitWidth: handle.implicitWidth
CustomRect {
id: handle
color: Colors.palette.m3secondary
implicitHeight: track.scrollBar.height * track.scrollBar.effectiveSize
implicitWidth: track.mouseArea.pressed || track.mouseArea.containsMouse ? Tokens.padding.extraSmall * 2 : Tokens.padding.extraSmall
anchors.right: parent.right
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;
color: Colors.palette.m3secondary
implicitHeight: track.scrollBar.height * track.scrollBar.effectiveSize
implicitWidth: track.mouseArea.pressed || track.mouseArea.containsMouse ? Tokens.padding.extraSmall * 2 : Tokens.padding.extraSmall
opacity: {
if (!track.scrollBar.enabled)
return 0;
}
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
if (track.scrollBar.size === 1)
return 0;
if (track.mouseArea.pressed)
return 1;
if (track.mouseArea.containsMouse)
return 0.8;
if (track.scrollBar.policy === CustomScrollBar.AlwaysOn || track.scrollBar.shouldBeActive)
return 0.6;
return 0;
}
radius: Tokens.rounding.full
y: track.scrollBar.reversed ? track.scrollBar.height * (1 + track.scrollBar.nonAnimY) * track.scrollBar.travelScale : track.scrollBar.height * track.scrollBar.nonAnimY * track.scrollBar.travelScale
Behavior on implicitWidth {
Anim {}
}
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
Behavior on implicitWidth {
Anim {}
}
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
MouseArea {
acceptedButtons: Qt.NoButton
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
}
MouseArea {
acceptedButtons: Qt.NoButton
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
}
}
}
+36 -39
View File
@@ -11,19 +11,18 @@ CustomMouseArea {
required property Item bar
required property real borderThickness
required property ScreenState screenState
property bool dashboardShortcutActive
property point dragStart
required property Drawing drawing
required property EdgeGeometry geometry
property bool osdShortcutActive
required property Panels panels
required property Wrapper popouts
required property ShellScreen screen
property bool dashboardShortcutActive
property bool osdShortcutActive
required property ScreenState screenState
property bool setInitialPoint: false
property bool singleGestureTriggered: false
property bool utilitiesShortcutActive
property bool setInitialPoint: false
property point dragStart
function inBottomPanel(panel: Item, x: real, y: real): bool {
const panelHeight = panel.height * (1 - (panel.offsetScale ?? 0));
@@ -60,39 +59,10 @@ CustomMouseArea {
return x >= panelX && x <= panelX + panel.width;
}
anchors.fill: parent
acceptedButtons: bar.fullscreen ? Qt.NoButton : Qt.AllButtons
anchors.fill: parent
hoverEnabled: true
onPressed: event => {
dragStart = Qt.point(event.x, event.y);
if (screenState.isDrawing) {
if (event.button & Qt.RightButton) {
drawing.content.clear();
return;
}
if (!inLeftPanel(panels.drawing, mouseX, mouseY))
drawing.content.beginStroke(event.x, event.y);
}
}
onPressedChanged: {
if (!pressed) {
if (screenState.isDrawing) {
setInitialPoint = false;
drawing.content.endStroke();
if (!inLeftPanel(panels.drawing, mouseX, mouseY))
drawing.content.showHover(mouseX, mouseY);
}
} else {
if (screenState.isDrawing) {
panels.drawing.collapse();
}
}
}
onContainsMouseChanged: {
if (!containsMouse) {
if (!osdShortcutActive) {
@@ -111,7 +81,6 @@ CustomMouseArea {
screenState.dashboard = false;
}
}
onPositionChanged: event => {
const x = event.x;
const y = event.y;
@@ -144,7 +113,8 @@ CustomMouseArea {
if (pressed && dragStart.y >= screen.height - Config.bar.border && dragStart.x > screen.width / 5 && dragY < -Config.launcher.dragThreshold)
screenState.launcher = true;
const showDashboard = Config.dashboard.showOnHover && inTopPanel(panels.dashboardWrapper, x, y);
const entryAt = bar.entryAt(geometry.axisPos(x, y));
const showDashboard = Config.dashboard.showOnHover && inTopPanel(panels.dashboardWrapper, x, y) && (!entryAt || entryAt === "activeWindow");
if (!dashboardShortcutActive) {
screenState.dashboard = showDashboard;
@@ -159,7 +129,7 @@ CustomMouseArea {
screenState.dashboard = false;
}
if (pressed && geometry.insetTop(borderThickness) > dragStart.y && (Config.dashboard.showOnHover || !inTopPanel(panels.dashboardWrapper, dragStart.x, dragStart.y))) {
if (pressed && geometry.insetTop(borderThickness) > dragStart.y && !inTopPanel(panels.dashboardWrapper, dragStart.x, dragStart.y)) {
if (dragY > 20) {
screenState.settings = true;
} else if (dragY < -20) {
@@ -212,6 +182,33 @@ CustomMouseArea {
if (geometry.barContains(x, y))
bar.checkPopout(geometry.axisPos(x, y));
}
onPressed: event => {
dragStart = Qt.point(event.x, event.y);
if (screenState.isDrawing) {
if (event.button & Qt.RightButton) {
drawing.content.clear();
return;
}
if (!inLeftPanel(panels.drawing, mouseX, mouseY))
drawing.content.beginStroke(event.x, event.y);
}
}
onPressedChanged: {
if (!pressed) {
if (screenState.isDrawing) {
setInitialPoint = false;
drawing.content.endStroke();
if (!inLeftPanel(panels.drawing, mouseX, mouseY))
drawing.content.showHover(mouseX, mouseY);
}
} else {
if (screenState.isDrawing) {
panels.drawing.collapse();
}
}
}
Timer {
id: unhideTimer
+6 -6
View File
@@ -37,12 +37,12 @@ Item {
readonly property alias resources: resources
readonly property alias resourcesWrapper: resourcesWrapper
required property ShellScreen screen
required property ScreenState screenState
readonly property alias settings: settings
readonly property alias settingsWrapper: settingsWrapper
readonly property alias sidebar: sidebar
readonly property alias toasts: toasts
readonly property alias utilities: utilities
required property ScreenState screenState
anchors.bottomMargin: geometry.insetBottom(borderThickness)
anchors.fill: parent
@@ -84,8 +84,8 @@ Item {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
screen: root.screen
sidebarOrSessionVisible: sidebar.visible
screenState: root.screenState
sidebarOrSessionVisible: sidebar.visible
}
}
@@ -121,8 +121,8 @@ Item {
anchors.right: parent.right
anchors.top: parent.top
panels: root
sidebarPanel: sidebar
screenState: root.screenState
sidebarPanel: sidebar
}
Launcher.Wrapper {
@@ -140,8 +140,8 @@ Item {
anchors.bottom: parent.bottom
anchors.right: parent.right
sidebar: sidebar
screenState: root.screenState
sidebar: sidebar
}
Item {
@@ -149,9 +149,9 @@ Item {
property real offsetScale: dashboard.shouldBeActive ? 0 : 1
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
clip: true
anchors.top: parent.top
clip: offsetScale > 0
implicitHeight: dashboard.implicitHeight * (1 - offsetScale)
implicitWidth: dashboard.implicitWidth
+43 -45
View File
@@ -52,15 +52,14 @@ CustomWindow {
property var root: Quickshell.shellDir
readonly property ScreenState screenState: ShellState.forScreen(screen)
readonly property real sdfBorderOffset: 2 * fsTransitionProg
readonly property real shadowOpacity: 0.7 * (1 - (bar.isHovered ? 0 : fsTransitionProg))
readonly property real shadowOpacity: 0.5 * (1 - (bar.isHovered ? 0 : fsTransitionProg))
property color surfaceColor: Colors.tPalette.m3surface
WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.keyboardFocus: screenState.launcher || screenState.settings || screenState.sidebar ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
WlrLayershell.keyboardFocus: screenState.screenshot ? WlrKeyboardFocus.Exclusive : screenState.launcher || screenState.settings || screenState.sidebar ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
WlrLayershell.layer: (fsTransitionProg > 0 && Config.general.showOverFullscreen) || (hasSpecialWorkspace && hasFullscreenOnNormalWs) ? WlrLayer.Overlay : WlrLayer.Top
color: "transparent"
contentItem.focus: true
mask: screenState.isDrawing ? null : (hasFullscreen && !bar.isHovered ? emptyRegion : regions)
mask: screenState.isDrawing || screenState.screenshot ? null : (hasFullscreen && !bar.isHovered ? emptyRegion : regions)
name: "Bar"
Behavior on fsTransitionProg {
@@ -143,7 +142,6 @@ CustomWindow {
HyprlandFocusGrab {
id: focusGrab
windows: [root]
active: {
const s = root.screenState;
const c = Config;
@@ -160,6 +158,7 @@ CustomWindow {
return true;
return false;
}
windows: [root]
onCleared: {
root.screenState.launcher = false;
@@ -185,23 +184,19 @@ CustomWindow {
id: surface
anchors.fill: parent
layer.enabled: true
opacity: root.surfaceColor.a
layer.effect: MultiEffect {
blurMax: 32
shadowColor: Qt.alpha(Colors.palette.m3shadow, Math.max(0, root.shadowOpacity))
shadowEnabled: true
}
BlobGroup {
BlobGroup2 {
id: blobGroup
anchors.fill: parent
color: root.surfaceColor
shadowColor: Qt.alpha(Colors.palette.m3shadow, Math.max(0, root.shadowOpacity))
shadowRadius: 18
smoothing: Config.bar.smoothing
}
BlobInvertedRect {
BlobInvertedRect2 {
anchors.fill: parent
anchors.margins: -50
borderBottom: geometry.insetBottom(root.borderThickness) - anchors.margins - root.sdfBorderOffset
@@ -251,6 +246,7 @@ CustomWindow {
PanelBg {
id: osdBg
atRestOverride: panels.osd.offsetScale < 0.99
deformAmount: 0.1
implicitHeight: panels.osd.height
implicitWidth: panels.osd.width
@@ -262,6 +258,7 @@ CustomWindow {
PanelBg {
id: notifsBg
atRestOverride: panel.implicitHeight > 0
panel: panels.notifications
radius: Tokens.rounding.large
}
@@ -296,13 +293,16 @@ CustomWindow {
PanelBg {
id: resourcesBg
property real extraExtent: 0.2
atRestOverride: panels.resources.offsetScale < 0.99
deformAmount: 0.05
implicitHeight: panels.resources.height
implicitWidth: panels.resources.width
implicitHeight: panels.resources.height * (1 + extraExtent)
implicitWidth: panels.resources.width * (1 + extraExtent)
panel: panels.resourcesWrapper
radius: Tokens.rounding.largeIncreased + Tokens.padding.small
x: panels.resourcesWrapper.x + panels.resources.x + geometry.insetLeft(root.borderThickness)
y: panels.resourcesWrapper.y + panels.resources.y + geometry.insetTop(root.borderThickness)
x: panels.resourcesWrapper.x + panels.resources.x + geometry.insetLeft(root.borderThickness) - panels.resources.width * extraExtent
y: panels.resourcesWrapper.y + panels.resources.y + geometry.insetTop(root.borderThickness) - panels.resources.height * extraExtent
}
PanelBg {
@@ -332,9 +332,15 @@ CustomWindow {
PanelBg {
id: clipboardBg
property real extraExtent: 0.2
deformAmount: 0.03
implicitHeight: panels.clipboard.height * (1 + extraExtent)
implicitWidth: panels.clipboard.width * (1 + extraExtent)
panel: panels.clipboard
radius: 29
x: panels.clipboard.x + geometry.insetLeft(root.borderThickness) - panels.clipboard.width * extraExtent
y: panels.clipboard.y + geometry.insetTop(root.borderThickness)
}
}
@@ -342,34 +348,8 @@ CustomWindow {
id: drawing
anchors.fill: parent
layer.enabled: true
screenState: root.screenState
z: 2
layer.effect: MultiEffect {
maskEnabled: true
maskInverted: true
maskSource: maskSource
}
}
Item {
id: maskSource
anchors.fill: parent
layer.enabled: true
visible: false
CustomRect {
readonly property int extraWidth: radius
color: "white"
implicitHeight: panels.drawing.height
implicitWidth: panels.drawing.width + extraWidth
radius: drawingBg.radius
x: -extraWidth + root.borderThickness + panels.drawing.x
y: panels.drawing.y + bar.implicitHeight
}
}
Interactions2 {
@@ -449,10 +429,28 @@ CustomWindow {
}
}
component PanelBg: BlobRect {
LazyLoader {
id: loader
property bool closing: !root.screenState.screenshot
onClosingChanged: if (!closing)
activeAsync = true
Picker {
loader: loader
parent: root.contentItem
screen: root.screen
screenState: root.screenState
}
}
component PanelBg: BlobRect2 {
property real deformAmount: 0.15
required property Item panel
atRestOverride: panel?.offsetScale < 0.99 ?? false
damping: 28.3
deformScale: (deformAmount * Config.appearance.deform.scale) / 10000
group: blobGroup
implicitHeight: panel.height
-88
View File
@@ -1,88 +0,0 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Wayland
import Quickshell.Io
import Quickshell.Hyprland
import ZShell
import qs.Components
Scope {
LazyLoader {
id: root
property bool closing
property bool freeze
Variants {
model: Quickshell.screens
PanelWindow {
id: win
required property ShellScreen modelData
WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.keyboardFocus: root.closing ? WlrKeyboardFocus.None : WlrKeyboardFocus.Exclusive
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.namespace: "areapicker"
color: "transparent"
mask: root.closing ? empty : null
screen: modelData
anchors {
bottom: true
left: true
right: true
top: true
}
Region {
id: empty
}
Picker {
loader: root
screen: win.modelData
}
}
}
}
IpcHandler {
function open(): void {
root.freeze = false;
root.closing = false;
root.activeAsync = true;
}
function openFreeze(): void {
root.freeze = true;
root.closing = false;
root.activeAsync = true;
}
target: "picker"
}
CustomShortcut {
name: "screenshot"
onPressed: {
root.freeze = false;
root.closing = false;
root.activeAsync = true;
}
}
CustomShortcut {
name: "screenshotFreeze"
onPressed: {
root.freeze = true;
root.closing = false;
root.activeAsync = true;
}
}
}
+82 -36
View File
@@ -41,6 +41,7 @@ Item {
property real rsy: Math.min(sy, ey)
readonly property real scaleRatio: Hypr.monitorFor(screen).scale
required property ShellScreen screen
required property ScreenState screenState
property real sh: Math.abs(sy - ey)
readonly property color shadowColor: Hypr.options.decoration.shadow?.color ?? "black"
readonly property bool shadowEnabled: Hypr.options.decoration.shadow.enabled
@@ -97,10 +98,6 @@ Item {
const args = Config.screenshot.mode === "auto" ? ["--rounding", `${rounding}`, "--radius", root.cornerRadius, "--shadow", root.shadowEnabled, "--shadow-blur", `${shadow_blur}`, "--shadow-color", `${r},${g},${b},${a}`, "--shadow-offset-x", root.shadowOffset[0], "--shadow-offset-y", root.shadowOffset[1]] : [];
const postCmd = Config.screenshot.enable_pp ? ["zshell-img-tools", "--scale", root.scaleRatio, ...args, "--image"] : null;
overlay.visible = border.visible = false;
handles.visible = false;
toolbar.visible = false;
ZShellIo.saveItem(captureLayer, tmpfile, Qt.rect(Math.ceil(rsx), Math.ceil(rsy), Math.floor(sw), Math.floor(sh)), path => {
const finalize = finalPath => {
if (copyToClipboard) {
@@ -129,22 +126,22 @@ Item {
Behavior on rsx {
enabled: !selectArea.pressed && root.mode === "select"
ExAnim {}
Anim {}
}
Behavior on rsy {
enabled: !selectArea.pressed && root.mode === "select"
ExAnim {}
Anim {}
}
Behavior on sh {
enabled: !selectArea.pressed && root.mode === "select"
ExAnim {}
Anim {}
}
Behavior on sw {
enabled: !selectArea.pressed && root.mode === "select"
ExAnim {}
Anim {}
}
Behavior on opacity {
Anim {
@@ -154,7 +151,7 @@ Item {
Component.onCompleted: {
Hypr.extras.refreshOptions();
screencopy.active = true;
root.forceActiveFocus();
opacity = 1;
@@ -219,13 +216,63 @@ Item {
onReleased: root.enterEditMode()
}
Connections {
function onClosingChanged(): void {
if (root.loader.closing)
closeAnim.start();
else {
closeAnim.stop();
returnAnim.start();
}
}
target: root.loader
}
ParallelAnimation {
id: returnAnim
Anim {
duration: 300
property: "opacity"
target: root
to: 1
}
Anim {
property: "rsx"
target: root
to: Math.min(root.sx, root.ex)
}
Anim {
property: "rsy"
target: root
to: Math.min(root.sy, root.ey)
}
Anim {
property: "sw"
target: root
to: Math.abs(root.sx - root.ex)
}
Anim {
property: "sh"
target: root
to: Math.abs(root.sy - root.ey)
}
}
SequentialAnimation {
id: closeAnim
PropertyAction {
property: "closing"
target: root.loader
value: true
ScriptAction {
script: {
const states = ShellState.allStates();
for (const s of states)
s.screenshot = false;
}
}
ParallelAnimation {
@@ -236,19 +283,19 @@ Item {
to: 0
}
ExAnim {
Anim {
properties: "rsx,rsy"
target: root
to: 0
}
ExAnim {
Anim {
property: "sw"
target: root
to: root.screen.width
}
ExAnim {
Anim {
property: "sh"
target: root
to: root.screen.height
@@ -267,17 +314,12 @@ Item {
anchors.fill: parent
Loader {
ScreencopyView {
id: screencopy
active: false
anchors.fill: parent
asynchronous: true
sourceComponent: ScreencopyView {
captureSource: root.screen
paintCursor: false
}
captureSource: root.screen
paintCursor: false
}
AnnotationCanvas {
@@ -286,6 +328,7 @@ Item {
anchors.fill: parent
inkColor: root.inkColor
layer.enabled: true
selection: selectionRect
tool: root.mode === "edit" && root.tool !== "move" ? root.tool : "none"
layer.effect: MultiEffect {
@@ -297,15 +340,14 @@ Item {
}
}
Rectangle {
CustomRect {
id: overlay
anchors.fill: parent
color: "white"
layer.enabled: true
opacity: 0.3
radius: root.realRounding
visible: screencopy.item?.hasContent ?? false
visible: screencopy.hasContent
layer.effect: MultiEffect {
maskEnabled: true
@@ -334,16 +376,15 @@ Item {
}
}
Rectangle {
CustomRect {
id: border
border.color: Colors.palette.m3primary
border.width: root.realBorderWidth
color: "transparent"
implicitHeight: selectionRect.implicitHeight + root.realBorderWidth * 2
implicitWidth: selectionRect.implicitWidth + root.realBorderWidth * 2
radius: root.realRounding > 0 ? root.realRounding + root.realBorderWidth : 0
visible: screencopy.item?.hasContent ?? false
visible: screencopy.hasContent
x: selectionRect.x - root.realBorderWidth
y: selectionRect.y - root.realBorderWidth
@@ -355,17 +396,24 @@ Item {
SelectionHandles {
id: handles
border: root.realBorderWidth
selH: root.sh
selW: root.sw
selX: root.rsx
selY: root.rsy
selection: selectionRect
tool: root.tool
visible: root.mode === "edit" && root.tool === "move"
onBoundsChanged: (x, y, w, h) => {
root.sx = x;
root.sy = y;
root.ex = x + w;
root.ey = y + h;
let nx = Math.max(0, x);
let ny = Math.max(0, y);
let nw = Math.min(w, root.screen.width - nx);
let nh = Math.min(h, root.screen.height - ny);
root.sx = nx;
root.sy = ny;
root.ex = nx + nw;
root.ey = ny + nh;
}
onMoveBy: (dx, dy) => {
const w = root.sw, h = root.sh;
@@ -396,6 +444,4 @@ Item {
onToolSelected: t => root.tool = t
onUndoRequested: annotations.undo()
}
component ExAnim: Anim {}
}
+51 -56
View File
@@ -8,13 +8,13 @@ Item {
property color inkColor: "#ff3b30"
property real inkWidth: 4
required property Rectangle selection
property string tool: "none"
signal changed
function clearAll() {
committedCanvas.clear();
previewCanvas.clear();
canvas.clear();
changed();
}
@@ -54,90 +54,85 @@ Item {
}
function undo() {
committedCanvas.undoLastGroup();
canvas.undoLastGroup();
changed();
}
anchors.fill: parent
enabled: tool !== "none"
StrokeCanvas {
id: committedCanvas
id: canvas
anchors.fill: parent
penColor: root.inkColor
penWidth: root.inkWidth
}
StrokeCanvas {
id: previewCanvas
anchors.fill: parent
penColor: root.inkColor
penWidth: root.inkWidth
}
PointHandler {
MouseArea {
id: drawHandler
property real startX
property real startY
property bool started: false
acceptedButtons: Qt.LeftButton | Qt.RightButton
enabled: root.tool !== "none"
anchors.fill: parent
cursorShape: undefined
enabled: root.enabled
onActiveChanged: {
if (active)
onPositionChanged: e => {
const x = e.x;
const y = e.y;
if (!pressed)
return;
if (root.tool === "pen") {
committedCanvas.endStroke();
} else if (started) {
const pts = root.sampleShape(root.tool, startX, startY, point.position.x, point.position.y);
if (pts.length > 0) {
committedCanvas.beginStroke(pts[0][0], pts[0][1], true);
for (let i = 1; i < pts.length; i++)
committedCanvas.appendPoint(pts[i][0], pts[i][1]);
committedCanvas.endStroke();
}
previewCanvas.clear();
}
started = false;
root.changed();
}
onPointChanged: {
if (!active)
canvas.appendPoint(x, y);
return;
}
if (point.pressedButtons & Qt.RightButton) {
root.clearAll();
return;
const pts = root.sampleShape(root.tool, startX, startY, x, y);
if (pts.length > 0) {
canvas.beginStroke(pts[0][0], pts[0][1], true);
for (let i = 1; i < pts.length; i++)
canvas.appendPoint(pts[i][0], pts[i][1]);
}
}
onPressed: e => {
if (e.button & Qt.LeftButton) {
startX = e.x;
startY = e.y;
canvas.beginStroke(e.x, e.y);
} else if (e.button & Qt.RightButton) {
root.clearAll();
}
}
onPressedChanged: if (!pressed)
canvas.endStroke()
}
HoverHandler {
cursorShape: {
if (root.tool === "none")
return undefined;
const x = point.position.x;
const y = point.position.y;
const withinXWidth = x > root.selection.x && x < root.selection.x + root.selection.implicitWidth;
const withinYHeight = y > root.selection.y && y < root.selection.y + root.selection.implicitHeight;
if (root.tool === "pen") {
if (!started) {
started = true;
committedCanvas.beginStroke(x, y);
return;
}
committedCanvas.appendPoint(x, y);
return;
}
if (!started) {
started = true;
startX = point.pressPosition.x;
startY = point.pressPosition.y;
}
const pts = root.sampleShape(root.tool, startX, startY, x, y);
if (pts.length > 0) {
previewCanvas.beginStroke(pts[0][0], pts[0][1], true);
for (let i = 1; i < pts.length; i++)
previewCanvas.appendPoint(pts[i][0], pts[i][1]);
if (withinXWidth && withinYHeight) {
if (!drawHandler.pressed)
canvas.showHover(x, y);
else
canvas.showHover(drawHandler.mouseX, drawHandler.mouseY);
return Qt.BlankCursor;
} else {
canvas.hideHover();
return undefined;
}
}
enabled: root.enabled
}
}
+40 -14
View File
@@ -1,18 +1,23 @@
pragma ComponentBehavior: Bound
import QtQuick
import ZShell.Internal
import qs.Services
Item {
id: root
property color handleBorder: "#3584e4"
property color handleFill: "white"
required property int border
property color handleBorder: Colors.palette.m3primary
property color handleFill: Colors.palette.m3onPrimary
property real hs: 12
property real minSize: 24
required property real selH
required property real selW
required property real selX
required property real selY
required property Rectangle selection
required property string tool
signal boundsChanged(x: real, y: real, w: real, h: real)
signal dragFinished
@@ -23,12 +28,16 @@ Item {
x: selX
y: selY
Component.onDestruction: Cursor.clearOverride()
DragHandler {
id: moveHandler
property real accumX: 0
property real accumY: 0
cursorShape: undefined
dragThreshold: 0
target: null
onActiveChanged: {
@@ -45,51 +54,63 @@ Item {
}
}
HoverHandler {
cursorShape: {
if (root.tool !== "move")
return undefined;
if ((moveHandler.centroid.pressedButtons & Qt.LeftButton) && moveHandler.active)
return Qt.ClosedHandCursor;
else
return Qt.OpenHandCursor;
}
}
Handle {
edge: "tl"
x: -root.hs / 2
y: -root.hs / 2
x: -(root.hs + root.border) / 2
y: -(root.hs + root.border) / 2
}
Handle {
edge: "t"
x: root.width / 2 - root.hs / 2
y: -root.hs / 2
y: -(root.hs + root.border) / 2
}
Handle {
edge: "tr"
x: root.width - root.hs / 2
y: -root.hs / 2
x: root.width - root.hs / 2 + root.border / 2
y: -(root.hs + root.border) / 2
}
Handle {
edge: "r"
x: root.width - root.hs / 2
x: root.width - root.hs / 2 + root.border / 2
y: root.height / 2 - root.hs / 2
}
Handle {
edge: "br"
x: root.width - root.hs / 2
y: root.height - root.hs / 2
x: root.width - root.hs / 2 + root.border / 2
y: root.height - root.hs / 2 + root.border / 2
}
Handle {
edge: "b"
x: root.width / 2 - root.hs / 2
y: root.height - root.hs / 2
y: root.height - root.hs / 2 + root.border / 2
}
Handle {
edge: "bl"
x: -root.hs / 2
y: root.height - root.hs / 2
x: -(root.hs + root.border) / 2
y: root.height - root.hs / 2 + root.border / 2
}
Handle {
edge: "l"
x: -root.hs / 2
x: -(root.hs + root.border) / 2
y: root.height / 2 - root.hs / 2
}
@@ -107,6 +128,8 @@ Item {
z: 10
HoverHandler {
id: hover
cursorShape: {
switch (handle.edge) {
case "tl":
@@ -132,6 +155,7 @@ Item {
property real startX
property real startY
dragThreshold: 0
grabPermissions: PointerHandler.CanTakeOverFromAnything | PointerHandler.TakeOverForbidden
target: null
@@ -141,8 +165,10 @@ Item {
startY = root.y;
startW = root.width;
startH = root.height;
Cursor.setOverride(hover.cursorShape);
} else {
root.dragFinished();
Cursor.clearOverride();
}
}
onTranslationChanged: {
+38
View File
@@ -5,7 +5,9 @@ import Quickshell
import Quickshell.Io
import QtQuick
import ZShell.Models
import ZShell.Internal
import ZShell.Config
import qs.Components
import qs.Services
import qs.Helpers
import qs.Paths
@@ -140,4 +142,40 @@ Searcher {
}
}
}
WallpaperService {
id: wallpaper
path: root.current
}
Variants {
model: Quickshell.screens
WallpaperObject {
readonly property var crops: root.getCrop(modelData.name)
required property var modelData
cropHeight: crops?.height ?? 1.0
cropWidth: crops?.width ?? 1.0
cropX: crops?.x ?? 0.0
cropY: crops?.y ?? 0.0
screen: modelData.name
service: wallpaper
zoom: crops?.zoom ?? 1.0
Behavior on cropHeight {
Anim {}
}
Behavior on cropWidth {
Anim {}
}
Behavior on cropX {
Anim {}
}
Behavior on cropY {
Anim {}
}
}
}
}
+4 -3
View File
@@ -17,12 +17,12 @@ GridLayout {
required property Wrapper popouts
required property ClipWrapper popoutsWrapper
required property ShellScreen screen
readonly property int vPadding: 6
required property ScreenState screenState
readonly property int vPadding: 6
function axisCenterOf(item: Item): real {
const c = item.mapToItem(root, item.implicitWidth / 2, item.implicitHeight / 2);
return horizontal ? c.x : c.y;
return horizontal ? Math.floor(c.x) : Math.floor(c.y);
}
function checkPopout(pos: real): void {
@@ -210,7 +210,8 @@ GridLayout {
roleValue: "network"
delegate: EntryWrapper {
NetworkWidget {}
NetworkWidget {
}
}
}
+31 -17
View File
@@ -1,6 +1,6 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Shapes
import Quickshell
import qs.Components
import ZShell.Config
import qs.Services
@@ -12,6 +12,7 @@ GridLayout {
property real animatedPercentage: 0
readonly property real arcStartAngle: 0.75 * Math.PI
readonly property real arcSweep: 1.5 * Math.PI
readonly property real dpr: (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1
required property bool horizontal
property string icon
required property color iconColor
@@ -22,12 +23,17 @@ GridLayout {
property bool warning: percentage * 100 >= warningThreshold
property int warningThreshold: 80
function snap(v) {
return Math.round(v * dpr) / dpr;
}
columnSpacing: Tokens.spacing.medium
columns: horizontal ? -1 : 1
percentage: 0
Behavior on animatedPercentage {
Anim {}
Anim {
}
}
Component.onCompleted: animatedPercentage = percentage
@@ -47,27 +53,35 @@ GridLayout {
text: root.icon
}
CustomClippingRect {
CustomRect {
id: track
Layout.preferredHeight: root.horizontal ? icon.implicitHeight - Tokens.padding.extraSmall : 4
Layout.preferredWidth: root.horizontal ? 4 : icon.implicitWidth
color: Colors.layer(Colors.palette.m3surfaceContainerHigh, 2)
radius: Tokens.rounding.full
radius: Math.min(width, height) / 2
CustomRect {
id: fill
Item {
id: clipper
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: root.horizontal ? parent.right : undefined
anchors.top: root.horizontal ? undefined : parent.top
color: root.mainColor
implicitHeight: root.horizontal ? Math.ceil(root.percentage * parent.height) : 0
implicitWidth: root.horizontal ? 0 : Math.ceil(root.percentage * parent.width)
anchors.bottom: root.horizontal ? parent.bottom : undefined
anchors.horizontalCenter: root.horizontal ? parent.horizontalCenter : undefined
anchors.left: root.horizontal ? undefined : parent.left
anchors.verticalCenter: root.horizontal ? undefined : parent.verticalCenter
clip: true
height: root.horizontal ? root.snap(root.percentage * track.height) : track.height * 2
width: root.horizontal ? track.width * 2 : root.snap(root.percentage * track.width)
// Behavior on implicitHeight {
// Anim {
// }
// }
CustomRect {
anchors.bottom: root.horizontal ? parent.bottom : undefined
anchors.horizontalCenter: root.horizontal ? parent.horizontalCenter : undefined
anchors.left: root.horizontal ? undefined : parent.left
anchors.verticalCenter: root.horizontal ? undefined : parent.verticalCenter
color: root.mainColor
height: track.height
radius: track.radius
width: track.width
}
}
}
}
+4 -2
View File
@@ -36,8 +36,8 @@ CustomClippingRect {
MaterialIcon {
id: noUpdatesIcon
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.centerIn: parent
anchors.verticalCenterOffset: -noUpdatesText.implicitHeight
color: Colors.tPalette.m3onSurfaceVariant
fontStyle: Tokens.font.icon.builders.large.size(48).weight(Font.DemiBold).build()
horizontalAlignment: Text.AlignHCenter
@@ -45,6 +45,8 @@ CustomClippingRect {
}
CustomText {
id: noUpdatesText
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: noUpdatesIcon.bottom
color: Colors.tPalette.m3onSurfaceVariant
+41 -65
View File
@@ -1,7 +1,6 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Widgets
import QtQuick
import QtQuick.Layouts
import ZShell.Config
@@ -11,11 +10,6 @@ import qs.Components
Item {
id: root
readonly property real nonAnimWidth: view.implicitWidth + viewWrapper.anchors.margins * 2
readonly property real nonAnimHeight: tabs.implicitHeight + tabs.anchors.topMargin + view.implicitHeight + viewWrapper.anchors.margins + viewWrapper.anchors.topMargin
required property ScreenState screenState
readonly property var dashboardTabs: {
const allTabs = [
{
@@ -45,9 +39,12 @@ Item {
];
return allTabs.filter(tab => tab.enabled);
}
readonly property real nonAnimHeight: tabs.implicitHeight + tabs.anchors.topMargin + view.implicitHeight + viewWrapper.anchors.margins + viewWrapper.anchors.topMargin
readonly property real nonAnimWidth: view.implicitWidth + viewWrapper.anchors.margins * 2
required property ScreenState screenState
implicitWidth: nonAnimWidth
implicitHeight: nonAnimHeight
implicitWidth: nonAnimWidth
Behavior on implicitHeight {
Anim {
@@ -63,15 +60,14 @@ Item {
Tabs {
id: tabs
anchors.top: parent.top
anchors.left: parent.left
anchors.margins: Tokens.padding.small
anchors.right: parent.right
anchors.top: parent.top
anchors.topMargin: ZUtils.clamp(anchors.margins - Config.bar.border, 0, anchors.margins)
nonAnimWidth: view.implicitWidth
screenState: root.screenState
tabs: root.dashboardTabs
anchors.topMargin: ZUtils.clamp(anchors.margins - Config.bar.border, 0, anchors.margins)
anchors.margins: Tokens.padding.small
}
Item {
@@ -79,15 +75,15 @@ Item {
property int radius: Tokens.rounding.extraLarge - anchors.margins
anchors.top: tabs.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.topMargin: Tokens.spacing.small
anchors.left: parent.left
anchors.margins: Tokens.padding.small
clip: true
anchors.right: parent.right
anchors.top: tabs.bottom
anchors.topMargin: Tokens.spacing.small
clip: view.currentIndex !== 3 || (!view.currentItem?.settled ?? true)
Flickable {
Item {
id: view
readonly property int currentIndex: root.screenState.dashboardTab
@@ -97,64 +93,50 @@ Item {
}
anchors.fill: parent
implicitWidth: currentItem?.implicitWidth ?? 0
implicitHeight: currentItem?.implicitHeight ?? 0
implicitWidth: currentItem?.implicitWidth ?? 0
contentX: currentItem?.x ?? 0
contentWidth: row.implicitWidth
contentHeight: row.implicitHeight
Repeater {
id: repeater
interactive: false
delegate: Loader {
id: paneLoader
onContentXChanged: {
if (!moving || !currentItem)
return;
required property int index
required property var modelData
const x = contentX - currentItem.x;
if (x > currentItem.implicitWidth / 2)
root.screenState.dashboardTab = Math.min(root.screenState.dashboardTab + 1, tabs.count - 1);
else if (x < -currentItem.implicitWidth / 2)
root.screenState.dashboardTab = Math.max(root.screenState.dashboardTab - 1, 0);
}
readonly property real targetX: index < view.currentIndex ? -view.implicitWidth : index > view.currentIndex ? view.implicitWidth : 0
readonly property bool settled: x === targetX
RowLayout {
id: row
active: index === view.currentIndex || opacity > 0
opacity: index === view.currentIndex ? 1 : 0
sourceComponent: modelData.component
visible: opacity > 0
x: targetX
y: 0
Repeater {
id: repeater
model: ScriptModel {
values: root.dashboardTabs
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
delegate: Loader {
id: paneLoader
required property int index
required property var modelData
Layout.alignment: Qt.AlignTop
sourceComponent: modelData.component
Component.onCompleted: active = Qt.binding(() => {
if (index === view.currentIndex)
return true;
const vx = Math.floor(view.visibleArea.xPosition * view.contentWidth);
const vex = Math.floor(vx + view.visibleArea.widthRatio * view.contentWidth);
return (vx >= x && vx <= x + implicitWidth) || (vex >= x && vex <= x + implicitWidth);
})
Behavior on x {
Anim {
type: Anim.DefaultEffects
}
}
}
model: ScriptModel {
values: root.dashboardTabs
}
}
Component {
id: dashComponent
Dash {
screenState: root.screenState
radius: viewWrapper.radius
screenState: root.screenState
}
}
@@ -170,8 +152,8 @@ Item {
id: mediaComponent
Media {
screenState: root.screenState
radius: viewWrapper.radius
screenState: root.screenState
}
}
@@ -182,12 +164,6 @@ Item {
radius: viewWrapper.radius
}
}
Behavior on contentX {
Anim {
type: Anim.DefaultEffects
}
}
}
}
}
@@ -171,7 +171,7 @@ Item {
Layout.alignment: ChatState.isWindow ? Qt.AlignCenter : Qt.AlignLeft | Qt.AlignVCenter
elide: Text.ElideRight
font: Tokens.font.title.medium
text: qsTr(root.chatData.title)
text: qsTr(root.chatData?.title || ChatState.title)
}
}
@@ -540,14 +540,17 @@ Item {
anchors.margins: Tokens.padding.medium
bg.border.color: Colors.palette.m3outlineVariant
bg.color: Colors.tPalette.m3surfaceContainerLowest
fastGlow: Chat.busy
fastGlow: LlmClient.busy
glow.opacity: input.activeFocus && (!LlmClient.busy || ChatState.isWindow)
font: Tokens.font.body.small
implicitHeight: Math.min(root.height / 5, contentHeight + topPadding + bottomPadding)
focus: true
placeholderText: qsTr("Send a message")
sendIcon.font: Tokens.font.icon.medium
sendIcon.icon: "arrow_upward"
sendIcon.icon: LlmClient.busy ? "stop" : "arrow_upward"
sendIcon.padding: Tokens.padding.extraSmall
sendIcon.opacity: text || LlmClient.busy ? 1.0 : 0.5
sendIcon.enabled: text || LlmClient.busy
Component.onCompleted: {
root.focusInput();
@@ -568,6 +571,11 @@ Item {
}
}
}
onSendPressed: root.send(text)
onButtonPressed: {
if (LlmClient.busy)
LlmClient.stop();
else
root.send(text);
}
}
}
@@ -134,7 +134,7 @@ CustomClippingRect {
anchors.leftMargin: readOnly ? 0 : Tokens.padding.large
anchors.top: parent.top
anchors.topMargin: readOnly ? Tokens.padding.small : Tokens.padding.extraSmall * 2
font: Tokens.font.title.medium
font: Tokens.font.body.medium
text: root.modelData.title || qsTr("New chat")
readOnly: true
color: Colors.palette.m3onSurface
@@ -161,10 +161,6 @@ CustomClippingRect {
CustomRect {
id: fadeMask
// anchors.top: titleText.top
// anchors.bottom: titleText.bottom
// anchors.left: titleText.left
// anchors.right: parent.right
anchors.fill: titleWrapper
layer.enabled: true
@@ -192,6 +188,7 @@ CustomClippingRect {
anchors.topMargin: Tokens.spacing.extraSmall
anchors.left: parent.left
anchors.right: parent.right
font: Tokens.font.label.small
color: Colors.palette.m3onSurfaceVariant
text: qsTr("Created on: %1").arg(root.modelData.createdAt.toLocaleString(Qt.locale("en_US"), "MMM d, yyyy - h:mm AP"))
opacity: root.expanded ? 1 : 0
@@ -207,6 +204,7 @@ CustomClippingRect {
anchors.top: created.bottom
anchors.topMargin: Tokens.spacing.small
anchors.left: parent.left
font: Tokens.font.label.small
color: Colors.palette.m3onSurfaceVariant
text: qsTr("Updated on: ")
opacity: root.expanded ? 1 : 0
@@ -223,6 +221,7 @@ CustomClippingRect {
anchors.top: title.bottom
anchors.topMargin: Tokens.spacing.extraSmall
color: Colors.palette.m3onSurfaceVariant
font: Tokens.font.label.small
text: root.modelData.updatedAt.toLocaleString(Qt.locale("en_US"), "MMM d, yyyy - h:mm AP")
states: State {
@@ -253,9 +252,10 @@ CustomClippingRect {
y: infoContainer.implicitHeight + topMargin
elide: Text.ElideRight
color: Colors.palette.m3onSurfaceVariant
font: Tokens.font.body.small
maximumLineCount: 1
opacity: !root.expanded && !root.slim ? 1 : 0
text: root.modelData.messagesModel.lastMessage?.activeGeneration.content.replace(/\s+/g, " ").trim() ?? qsTr("No messages yet")
text: root.modelData.messagesModel.lastMessage?.content.replace(/\s+/g, " ").trim() ?? qsTr("No messages yet")
Behavior on y {
Anim {}
@@ -280,7 +280,7 @@ CustomClippingRect {
color: Colors.palette.m3onSurfaceVariant
maximumLineCount: 5
opacity: root.expanded ? 1 : 0
text: root.modelData.messagesModel.lastMessage?.activeGeneration.content ?? qsTr("No messages yet")
text: root.modelData.messagesModel.lastMessage?.content ?? qsTr("No messages yet")
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
Behavior on y {
@@ -8,10 +8,11 @@ TextAreaBase {
readonly property alias bg: bg
readonly property alias sendIcon: sendIcon
readonly property alias glow: glow
property bool fastGlow: false
signal sendPressed
signal buttonPressed
bottomPadding: Tokens.padding.large
leftPadding: Tokens.padding.extraLarge
@@ -39,6 +40,7 @@ TextAreaBase {
GeminiGlow {
id: glow
glowSize: 32
z: 5
opacity: root.activeFocus ? 1 : 0
visible: opacity > 0
@@ -131,7 +133,7 @@ TextAreaBase {
}
}
onClicked: root.sendPressed()
onClicked: root.buttonPressed()
}
TapHandler {
+22 -155
View File
@@ -5,6 +5,7 @@ import QtQuick
import QtQuick.Layouts
import ZShell.Llm
import ZShell.Config
import qs.Modules.Notifications.Sidebar.Chat.Content
import qs.Services
import qs.Components
@@ -21,15 +22,22 @@ Item {
signal loadChatRequest(content: ChatSession, index: int)
signal newChatRequest
onSlimChanged: {
if (slim)
modelsContainer.open = false;
}
CustomText {
id: header
id: chatHeader
anchors.left: parent.left
anchors.margins: Tokens.padding.extraSmall
anchors.right: parent.right
anchors.top: parent.top
color: Colors.palette.m3outline
font: Tokens.font.label.large
font.family: "CaskaydiaCove NF"
font.pointSize: 13
font.weight: 500
text: qsTr("%1 Chat%2").arg(list.count).arg(list.count > 1 ? "s" : "")
}
@@ -39,7 +47,7 @@ Item {
anchors.bottom: modelsContainer.top
anchors.left: parent.left
anchors.right: parent.right
anchors.top: header.bottom
anchors.top: chatHeader.bottom
anchors.topMargin: Tokens.spacing.medium
clip: true
spacing: Tokens.spacing.medium
@@ -96,156 +104,24 @@ Item {
}
}
CustomClippingRect {
ModelsContainer {
id: modelsContainer
property bool expanded: false
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: fabRoot.left
anchors.margins: Tokens.padding.small
anchors.rightMargin: Tokens.spacing.small
implicitHeight: expanded ? 400 : fabRoot.implicitHeight
radius: modelLayer.pressed ? Tokens.rounding.small : modelName.implicitHeight / 2
rootParent: root
fabRoot: fabRoot
function prettyModelName(): string {
if (modelsList.count < 1)
return qsTr("No models found");
opacity: root.slim ? 0 : 1
visible: opacity > 0
const pathList = Chat.model.split("\/");
const name = pathList[pathList.length - 1];
if (!name)
return qsTr("No model selected");
return name;
}
Behavior on implicitHeight {
Anim {}
}
Behavior on radius {
Anim {}
}
Item {
id: modelName
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
implicitHeight: fabRoot.implicitHeight
CustomText {
id: label
text: modelsContainer.prettyModelName()
color: Colors.palette.m3onSurfaceVariant
anchors.left: parent.left
anchors.leftMargin: Tokens.padding.large
anchors.verticalCenter: parent.verticalCenter
}
MaterialIcon {
color: Colors.palette.m3onSurfaceVariant
anchors.right: parent.right
anchors.rightMargin: Tokens.padding.large
anchors.verticalCenter: parent.verticalCenter
text: modelsContainer.expanded ? "unfold_less" : "unfold_more"
animate: true
fontStyle: Tokens.font.icon.medium
visible: modelsList.count > 0
}
StateLayer {
id: modelLayer
enabled: modelsList.count > 0
onClicked: {
ChatState.fabExpanded = false;
modelsContainer.expanded = !modelsContainer.expanded;
}
radius: modelsContainer.radius
}
}
VerticalFadeListView {
id: modelsList
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.top: modelName.bottom
anchors.margins: Tokens.padding.medium
anchors.topMargin: Tokens.spacing.medium
model: Chat.availableModels
opacity: modelsContainer.expanded ? 1 : 0
currentIndex: Chat.availableModels.indexOf(Chat.model)
visible: opacity > 0
Behavior on opacity {
Anim {}
}
CustomScrollBar.vertical: CustomScrollBar {
flickable: modelsList
}
delegate: CustomRect {
id: model
required property var modelData
required property int index
readonly property bool selected: model.ListView.view.currentIndex === model.index
implicitWidth: ListView.view.width
implicitHeight: layout.implicitHeight + Tokens.padding.small * 2
radius: stateLayer.pressed ? Tokens.rounding.extraSmall : selected ? Tokens.rounding.largeIncreased : Tokens.rounding.medium
color: Qt.alpha(Colors.palette.m3tertiaryContainer, selected ? 1 : 0)
Behavior on radius {
Anim {
type: Anim.SlowEffects
}
}
RowLayout {
id: layout
anchors.fill: parent
anchors.margins: Tokens.padding.small
anchors.leftMargin: Tokens.padding.large
anchors.rightMargin: Tokens.padding.large
CustomText {
text: model.modelData
color: model.selected ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurface
}
MaterialIcon {
Layout.alignment: Qt.AlignRight
visible: model.selected
color: Colors.palette.m3onTertiaryContainer
fontStyle: Tokens.font.icon.medium
opacity: model.selected ? 1 : 0
text: "check"
Behavior on opacity {
Anim {
type: Anim.SlowEffects
}
}
}
}
StateLayer {
id: stateLayer
onClicked: {
Chat.selectModel(model.modelData);
}
}
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
}
@@ -257,22 +133,14 @@ Item {
anchors.right: parent.right
anchors.margins: Tokens.padding.small
padding: 8
font: Tokens.font.icon.large
font.pointSize: Math.round(18 * 1.2)
icon: root.slim ? "left_panel_open" : "add"
isRound: ChatState.fabExpanded
// opacity: root.slim ? 0 : 1
// visible: opacity > 0
//
// Behavior on opacity {
// Anim {
// type: Anim.DefaultEffects
// }
// }
label.transform: Rotation {
origin.y: fabRoot.label.height / 2
origin.x: fabRoot.label.width / 2
angle: ChatState.fabExpanded ? 135 : 0
angle: ChatState.fabExpanded && (ChatState.narrowSidebarExpanded && !ChatState.isWide || !ChatState.isWindow) ? 135 : 0
Behavior on angle {
Anim {}
@@ -285,7 +153,6 @@ Item {
return;
}
modelsContainer.expanded = false;
ChatState.fabExpanded = !ChatState.fabExpanded;
}
@@ -403,7 +270,7 @@ Item {
MaterialIcon {
text: fabMenuItem.modelData.icon
color: Colors.palette.m3onPrimaryContainer
fontStyle: Tokens.font.icon.medium
fontStyle: Tokens.font.icon.large
fill: 1
}
@@ -17,15 +17,26 @@ Item {
anchors.margins: Tokens.padding.small
Component.onCompleted: {
if (ChatState.firstOpen) {
ChatState.openDraft();
stack.push(chatList);
stack.push(chatContent);
ChatState.firstOpen = false;
return;
}
if (ChatState.inChat) {
stack.push(chatList);
stack.push(chatContent, {
"chatData": ChatState.chatSession
});
stack.push(chatContent);
} else
stack.push(chatList);
}
function openChat(): void {
if (stack.depth < 2)
stack.push(chatContent);
}
StackView {
id: stack
anchors.fill: parent
@@ -35,27 +46,23 @@ Item {
id: chatList
ChatList {
model: ScriptModel {
values: Chat.chats.values
}
model: LlmClient.chats
onDeleteChatRequest: chat => {
Chat.chats.remove(chat);
const wasCurrent = chat === ChatState.chatSession;
LlmClient.chats.remove(chat);
if (wasCurrent)
ChatState.openDraft();
}
onLoadChatRequest: (chat, index) => {
stack.push(chatContent, {
"chatData": chat
});
ChatState.inChat = true;
ChatState.chatSession = chat;
openChat();
ChatState.inChat = true;
}
onNewChatRequest: {
const data = Chat.chats.insert();
stack.push(chatContent, {
"chatData": data
});
ChatState.openDraft();
openChat();
ChatState.inChat = true;
ChatState.chatSession = data;
}
}
}
@@ -64,9 +71,12 @@ Item {
id: chatContent
ChatContent {
chatData: ChatState.chatSession
onRequestClose: {
stack.pop();
ChatState.inChat = false;
ChatState.openDraft();
}
}
}
@@ -8,9 +8,10 @@ import qs.Services
Singleton {
id: root
property ChatSession chatSession
readonly property string title: chatSession ? chatSession.title : "Chat"
property ChatSession chatSession: LlmClient.chats.draft()
readonly property string title: chatSession?.title || "Chat"
property int currentIdx: -1
property bool firstOpen: true
property bool inChat: false
property bool fabExpanded: false
property bool isWindow: false
@@ -22,6 +23,11 @@ Singleton {
property ShellScreen screen
property bool sidebarVisible: ShellState.forActive().sidebar
function openDraft(): void {
root.chatSession = LlmClient.chats.draft();
root.currentIdx = -1;
}
onSidebarVisibleChanged: if (!sidebarVisible)
fabExpanded = false
}
@@ -9,12 +9,11 @@ import qs.Services
Item {
id: root
required property ChatGeneration current
required property TextEdit edit
required property bool hovered
required property bool isUser
required property ChatMessage message
required property LlmSegment segment
required property bool running
implicitHeight: editButton.implicitHeight
implicitWidth: generationTools.implicitWidth + editTools.implicitWidth + Tokens.spacing.small
@@ -29,6 +28,7 @@ Item {
enabled: root.message.generationCount > 1 && root.message.activeGenerationIndex !== 0
icon: "chevron_left"
type: IconButton.Tonal
isRound: true
onClicked: {
if (!root.edit.readOnly)
@@ -40,7 +40,7 @@ Item {
IconButton {
enabled: false
icon: `${root.message.activeGenerationIndex + 1}`
label.font: Tokens.font.label.small
label.font: Tokens.font.body.small
type: IconButton.Text
}
@@ -48,6 +48,7 @@ Item {
enabled: root.message.generationCount > 1 && root.message.activeGenerationIndex !== root.message.generationCount - 1
icon: "chevron_right"
type: IconButton.Tonal
isRound: true
onClicked: {
if (!root.edit.readOnly)
@@ -61,7 +62,7 @@ Item {
id: editTools
anchors.right: parent.right
opacity: !root.current.streaming && root.hovered ? 1 : 0
opacity: !root.running && root.hovered ? 1 : 0
spacing: Tokens.spacing.small
visible: opacity > 0
@@ -75,6 +76,7 @@ Item {
inactiveOnColor: Colors.palette.m3onSecondary
scale: root.edit.readOnly ? 1 : 0
visible: !root.isUser
isRound: true
Behavior on scale {
Anim {}
@@ -90,12 +92,13 @@ Item {
inactiveColor: Colors.palette.m3secondary
inactiveOnColor: Colors.palette.m3onSecondary
scale: root.edit.readOnly ? 1 : 0
isRound: true
Behavior on scale {
Anim {}
}
onClicked: Quickshell.clipboardText = root.current.content
onClicked: Quickshell.clipboardText = root.message.content
}
IconButton {
@@ -105,6 +108,7 @@ Item {
inactiveColor: Colors.palette.m3tertiary
inactiveOnColor: Colors.palette.m3onTertiary
visible: root.isUser
isRound: true
onClicked: {
root.edit.readOnly = !root.edit.readOnly;
@@ -0,0 +1,400 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import ZShell.Config
import ZShell.Components
import ZShell.Llm
import ZShell.Blobs
import qs.Components
import qs.Services
Item {
id: root
required property Item rootParent
required property ChatGeneration current
implicitHeight: approvalLayout.implicitHeight + approvalLayout.anchors.margins * 2
CustomRect {
id: approvalBg
anchors.fill: parent
radius: Tokens.rounding.medium
color: Colors.palette.m3surface
border.width: 1
border.color: Colors.palette.m3outlineVariant
ColumnLayout {
id: approvalLayout
anchors.fill: parent
anchors.margins: Tokens.padding.medium
spacing: Tokens.spacing.small
CustomText {
id: requestHeader
text: qsTr("Tool call request")
font: Tokens.font.headline.medium
color: Colors.palette.m3onSurface
}
Item {
id: toolsBg
property bool open: false
property int maxHeight: root.rootParent.height - Tokens.padding.medium * 2
property int openHeight: Math.min(toolsContent.item?.implicitHeight, maxHeight)
property int openWidth: root.rootParent.width - Tokens.padding.medium * 2
property real openHeightProg: openHeight / maxHeight
property int collapsedY: 0
implicitHeight: toolList.implicitHeight + toolList.anchors.margins * 2
z: open || dialogTransition.running ? 2 : 0
Layout.fillWidth: true
function reparentWrapper(): void {
const newParent = open ? root.rootParent : toolsBg;
const c = toolsWrapper.parent.mapToItem(newParent, toolsWrapper.x + toolsWrapper.width / 2, toolsWrapper.y + toolsWrapper.height / 2);
toolsBg.collapsedY = toolsWrapper.y;
toolsWrapper.parent = newParent;
toolsWrapper.x = c.x - toolsWrapper.width / 2;
toolsWrapper.y = c.y - toolsWrapper.height / 2;
}
onOpenChanged: {
if (open)
root.rootParent.interactive = false;
else
root.rootParent.interactive = true;
}
BlobGroup {
id: blobGroup
color: toolsBg.open ? Colors.palette.m3surfaceContainerHighest : Colors.tPalette.m3primaryContainer
Behavior on color {
CAnim {}
}
}
MouseArea {
id: backdrop
anchors.fill: parent
enabled: false
hoverEnabled: enabled
preventStealing: true
parent: toolsBg.open ? root.rootParent : toolsBg
onClicked: toolsBg.open = false
}
Item {
id: toolsWrapper
width: toolsBg.width
height: toolBox.implicitHeight
rotation: parent === root.rootParent ? 180 : 0
z: 1
states: State {
name: "open"
when: toolsBg.open
PropertyChanges {
backdrop.enabled: true
toolsBackground.bottomLeftRadius: Tokens.rounding.largeIncreased
toolsBackground.bottomRightRadius: Tokens.rounding.largeIncreased
toolsBackground.topRightRadius: Tokens.rounding.largeIncreased
toolsBackground.topLeftRadius: Tokens.rounding.largeIncreased
toolsContent.opacity: 1
toolsWrapper.height: toolsBg.openHeight
toolsWrapper.width: toolsBg.openWidth
toolsWrapper.x: (root.rootParent.width - toolsBg.openWidth) / 2
toolsWrapper.y: (confirmationRow.implicitHeight + Tokens.spacing.small + Tokens.padding.medium) * (1 - toolsBg.openHeightProg) + Tokens.padding.medium
elevation.opacity: 1
toolBox.opacity: 0
}
}
transitions: Transition {
id: dialogTransition
SequentialAnimation {
ScriptAction {
script: toolsBg.reparentWrapper()
}
Anim {
properties: "x,y"
}
}
PropertyAction {
property: "enabled"
}
Anim {
properties: "opacity,topLeftRadius,topRightRadius,bottomLeftRadius,bottomRightRadius"
type: Anim.DefaultEffects
}
Anim {
properties: "width,height"
}
}
Elevation {
id: elevation
anchors.fill: parent
bottomLeftRadius: toolsBackground.bottomLeftRadius
bottomRightRadius: toolsBackground.bottomRightRadius
level: 4
opacity: 0
radius: toolsBackground.radius
transform: Matrix4x4 {
matrix: toolsBackground.deformMatrix
}
}
BlobRect {
id: toolsBackground
anchors.fill: parent
bottomLeftRadius: toolsBg.open ? Tokens.rounding.largeIncreased : Tokens.rounding.small
bottomRightRadius: toolsBg.open ? Tokens.rounding.largeIncreased : Tokens.rounding.small
topRightRadius: toolsBg.open ? Tokens.rounding.largeIncreased : Tokens.rounding.small
topLeftRadius: toolsBg.open ? Tokens.rounding.largeIncreased : Tokens.rounding.small
deformScale: 0
group: blobGroup
opacity: blobGroup.color.a * (root.enabled ? 1 : 0.5)
}
CustomRect {
id: toolBox
anchors.left: parent.left
anchors.right: parent.right
height: Math.min(implicitHeight, parent.height)
implicitHeight: toolList.implicitHeight + Tokens.padding.medium * 2
radius: Tokens.rounding.small
StateLayer {
onClicked: {
toolsBg.open = true;
}
}
ColumnLayout {
id: toolList
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Tokens.padding.medium
Repeater {
id: repeater
model: root.current.pendingToolCalls?.slice(0, 4) ?? []
delegate: RowLayout {
id: toolText
required property var modelData
required property int index
spacing: Tokens.spacing.medium
function toolTarget(seg: var): string {
var a = null;
try {
a = JSON.parse(seg.arguments);
} catch (e) {}
if (a && typeof a === "object") {
const v = a.url ?? a.path;
if (v !== undefined && v !== null)
return String(v);
}
return String(seg.arguments);
}
MaterialIcon {
color: Colors.palette.m3onPrimaryContainer
text: toolText.modelData.name === "webfetch" ? "web" : "text_snippet"
fill: 1
}
CustomText {
color: Colors.palette.m3onPrimaryContainer
text: qsTr("%1").arg(toolText.modelData.name)
}
MaterialIcon {
color: Colors.palette.m3onPrimaryContainer
text: "arrow_right_alt"
}
CustomText {
color: Colors.palette.m3onPrimaryContainer
text: qsTr("%1").arg(toolText.toolTarget(toolText.modelData))
}
}
}
CustomText {
color: Colors.palette.m3onPrimaryContainer
text: qsTr("And %1 more").arg(root.current.pendingToolCalls.slice(4).length)
visible: repeater.count < root.current.pendingToolCalls.length
}
}
}
Loader {
id: toolsContent
active: opacity > 0
anchors.fill: parent
asynchronous: false
opacity: 0
sourceComponent: MouseArea {
onWheel: event => event.accepted = true
preventStealing: true
implicitHeight: innerLayout.contentHeight + innerLayout.anchors.margins * 2
VerticalFadeListView {
id: innerLayout
anchors.fill: parent
anchors.margins: Tokens.padding.small
cacheBuffer: height * 10
fadeAmount: 0.1
fadeEnabled: interactive
spacing: 0
clip: true
interactive: contentHeight > toolsBg.maxHeight
model: root.current.pendingToolCalls ?? []
delegate: Item {
id: toolCall
required property var modelData
property bool expanded: false
implicitWidth: CustomListView.view.width
implicitHeight: collapsedRow.implicitHeight + collapsedRow.anchors.margins + (expanded ? expandedTool.implicitHeight + expandedTool.anchors.margins + expandedTool.anchors.topMargin : collapsedRow.anchors.margins)
function toolTarget(seg: var): string {
var a = null;
try {
a = JSON.parse(seg.arguments);
} catch (e) {}
if (a && typeof a === "object") {
const v = a.url ?? a.path;
if (v !== undefined && v !== null)
return String(v);
}
return String(seg.arguments);
}
Behavior on implicitHeight {
Anim {}
}
RowLayout {
id: collapsedRow
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Tokens.padding.medium
IconButton {
id: toolExpandBtn
font: Tokens.font.icon.medium
icon: "keyboard_arrow_down"
inactiveOnColor: hovered ? Colors.palette.m3onSurface : Colors.palette.m3outline
rotation: toolCall.expanded ? 180 : 0
type: IconButton.Text
Behavior on rotation {
Anim {}
}
onClicked: toolCall.expanded = !toolCall.expanded
}
CustomText {
id: preview
Layout.fillWidth: true
text: toolCall.modelData.name
}
}
CustomClippingRect {
id: expandedTool
anchors.top: collapsedRow.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: Tokens.padding.medium
anchors.topMargin: Tokens.spacing.medium
implicitHeight: fullToolCall.implicitHeight + fullToolCall.anchors.margins * 2
radius: Tokens.rounding.medium
color: Colors.palette.m3surfaceContainerLow
CustomText {
id: fullToolCall
anchors.fill: parent
anchors.margins: Tokens.padding.medium
text: toolCall.toolTarget(toolCall.modelData)
wrapMode: CustomText.WrapAtWordBoundaryOrAnywhere
}
}
}
}
}
}
}
}
ButtonRow {
id: confirmationRow
Layout.alignment: Qt.AlignRight
spacing: Tokens.spacing.medium
IconTextButton {
icon: "check_circle"
text: qsTr("Allow")
horizontalPadding: Tokens.padding.large
verticalPadding: Tokens.padding.small
isRound: true
shapeMorph: true
onClicked: root.current.approveTools()
}
IconTextButton {
icon: "cancel"
text: qsTr("Deny")
isRound: true
shapeMorph: true
inactiveColor: Colors.palette.m3error
inactiveOnColor: Colors.palette.m3onError
horizontalPadding: Tokens.padding.large
verticalPadding: Tokens.padding.small
onClicked: root.current.denyTools()
}
}
}
}
}
@@ -34,8 +34,12 @@ Item {
});
const attach = () => {
incubator.object.anchors.fill = container;
incubator.object.anchors.top = container.top;
incubator.object.anchors.bottom = container.bottom;
incubator.object.anchors.margins = Tokens.padding.medium;
incubator.object.anchors.horizontalCenter = container.horizontalCenter;
incubator.object.requestClose.connect(root.requestClose);
incubator.object.implicitWidth = Qt.binding(() => Math.max(Math.min(container.width - Tokens.padding.medium * 2, 800), Config.sidebar.sizes.width));
currentItem = incubator.object;
root.loading = false;
if (!noAnim)
@@ -66,7 +70,7 @@ Item {
LoadingIndicator {
anchors.centerIn: parent
implicitSize: Tokens.font.icon.large.pointSize * 4
implicitSize: Tokens.font.icon.extraLarge.pointSize * 4
opacity: root.loading ? 1 : 0
visible: opacity > 0
@@ -10,12 +10,13 @@ Item {
id: root
required property bool isUser
required property LlmSegment segment
required property string text
required property var blocks
required property bool running
required property bool hovered
required property Repeater repeater
required property int index
required property ChatMessage message
required property ChatGeneration current
readonly property real contentMaxWidth: width - Tokens.spacing.extraSmall - Tokens.spacing.extraLarge
@@ -34,7 +35,7 @@ Item {
radius: Tokens.rounding.medium
color: root.isUser ? Colors.palette.m3primary : Colors.palette.m3surfaceContainer
implicitWidth: layoutFrozen ? frozenWidth : layoutWidth
implicitHeight: root.isUser ? msgText.contentHeight + Tokens.padding.medium * 2 : blocks.implicitHeight + blocks.anchors.topMargin * 2
implicitHeight: root.isUser ? msgText.contentHeight + Tokens.padding.medium * 2 : markdown.implicitHeight + markdown.anchors.topMargin * 2
anchors.right: root.isUser ? parent.right : undefined
layer.enabled: false
layer.smooth: true
@@ -74,7 +75,7 @@ Item {
TextEditBase {
id: msgText
property string cachedText: root.segment.text
property string cachedText: root.text
property bool cancelled: false
visible: root.isUser
@@ -88,7 +89,7 @@ Item {
font: Tokens.font.body.small
readOnly: true
selectionColor: Qt.alpha((root.isUser ? Colors.palette.m3onPrimary : Colors.palette.m3primary), 0.4)
text: root.isUser ? root.segment.text : ""
text: root.isUser ? root.text : ""
textFormat: CustomText.MarkdownText
wrapMode: Text.WordWrap
@@ -129,23 +130,22 @@ Item {
}
}
// Assistant content: parsed markdown blocks.
MarkdownBlocks {
id: blocks
id: markdown
visible: !root.isUser
anchors.topMargin: Tokens.padding.medium
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
blocks: root.segment.markdown
blocks: root.blocks
}
}
Actions {
id: actionsRow
readonly property bool shouldBeActive: (root.segment.type === LlmSegment.Type.Content) && !root.segment.running && root.repeater.count === (root.index + 1)
readonly property bool shouldBeActive: !root.running && root.repeater.count === (root.index + 1)
anchors.left: parent.left
anchors.right: (implicitWidth > bubble.implicitWidth) ? undefined : bubble.right
@@ -156,8 +156,7 @@ Item {
edit: msgText
hovered: root.hovered
isUser: root.isUser
segment: root.segment
current: root.current
running: root.running
message: root.message
Behavior on opacity {
@@ -16,7 +16,7 @@ ColumnLayout {
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
color: Colors.tPalette.m3outlineVariant
fontStyle: Tokens.font.icon.large
fontStyle: Tokens.font.icon.extraLarge
text: "chat"
}
@@ -93,7 +93,7 @@ Item {
Layout.fillWidth: true
font: Tokens.font.title.large
font: Tokens.font.headline.small
elide: CustomText.ElideRight
color: Colors.palette.m3onErrorContainer
@@ -108,7 +108,7 @@ Item {
wrapMode: CustomText.WordWrap
color: Colors.palette.m3onErrorContainer
text: root.current.errorMessage || Chat.serverError
text: root.current.errorMessage || LlmClient.serverError
}
}
@@ -23,10 +23,10 @@ Column {
role: "type"
DelegateChoice {
roleValue: LlmMarkdown.Type.Code
roleValue: MarkdownBlock.Type.Code
delegate: CodeBlockView {
required property var modelData
required property MarkdownBlock modelData
language: modelData.language
code: modelData.code
@@ -37,10 +37,10 @@ Column {
}
DelegateChoice {
roleValue: LlmMarkdown.Type.Math
roleValue: MarkdownBlock.Type.Math
delegate: MathBlockView {
required property var modelData
required property MarkdownBlock modelData
latex: modelData.latex
anchors.right: parent.right
@@ -50,10 +50,10 @@ Column {
}
DelegateChoice {
roleValue: LlmMarkdown.Type.Heading
roleValue: MarkdownBlock.Type.Heading
delegate: TextEditBase {
required property var modelData
required property MarkdownBlock modelData
color: Colors.palette.m3onSurface
anchors.right: parent.right
@@ -64,20 +64,20 @@ Column {
readOnly: true
font: {
if (modelData.level <= 1)
return Tokens.font.headline.builders.small.weight(Font.Bold).build();
return Tokens.font.title.builders.large.weight(Font.Bold).build();
if (modelData.level === 2)
return Tokens.font.headline.builders.small.size(18).weight(Font.DemiBold).build();
return Tokens.font.body.medium;
return Tokens.font.title.builders.medium.weight(Font.Bold).build();
return Tokens.font.title.builders.small.weight(Font.Bold).build();
}
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
}
}
DelegateChoice {
roleValue: LlmMarkdown.Type.Text
roleValue: MarkdownBlock.Type.Text
delegate: BubbleEdit {
required property var modelData
required property MarkdownBlock modelData
anchors.right: parent.right
anchors.left: parent.left
@@ -9,7 +9,7 @@ Item {
required property string latex
property color mathColor: Colors.palette.m3onSurface
property real fontSize: Tokens.font.body.large.pointSize
property font font: Tokens.font.headline.medium
implicitHeight: Math.max(equationImage.height, fallbackText.contentHeight) + Tokens.padding.small * 2
@@ -18,7 +18,7 @@ Item {
latex: root.latex
color: root.mathColor
fontPointSize: root.fontSize
fontPointSize: root.font.pointSize
devicePixelRatio: Screen.devicePixelRatio
}
@@ -39,7 +39,7 @@ Item {
anchors.centerIn: parent
color: root.mathColor
font: Tokens.font.mono.small
font: Tokens.font.mono.medium
text: root.latex
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
visible: !math.ok
@@ -19,7 +19,16 @@ MouseArea {
required property ChatListView chatList
readonly property bool isUser: modelData.role === ChatMessage.Role.User
readonly property var blocks: blockify(current.segments, current.error)
readonly property var blocks: root.isUser ? [
{
kind: "content",
id: 0,
text: root.modelData.content,
markdown: [],
running: false
}
] : blockify(current.segments, current.error)
property ChatGeneration current: modelData.activeGeneration
property bool reasoningExpanded: false
@@ -54,7 +63,9 @@ MouseArea {
blocks.push({
kind: "content",
id: i,
segments: [seg]
text: seg.text,
markdown: seg.markdown,
running: seg.running
});
} else {
const last = blocks.length ? blocks[blocks.length - 1] : null;
@@ -231,7 +242,7 @@ MouseArea {
MaterialIcon {
anchors.centerIn: parent
fontStyle: Tokens.font.icon.large
fontStyle: Tokens.font.icon.extraLarge
text: thumb.fileIcon(thumb.modelData?.mimeType ?? "")
visible: !thumb.modelData?.isImage ?? false
}
@@ -330,11 +341,12 @@ MouseArea {
required property var modelData
isUser: root.isUser
segment: modelData.segments[0]
text: modelData.text
blocks: modelData.markdown
running: modelData.running
width: root.width
repeater: segmentRep
message: root.modelData
current: root.current
hovered: root.containsMouse
onEdit: text => {
@@ -0,0 +1,351 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import ZShell.Config
import ZShell.Components
import ZShell.Llm
import ZShell.Blobs
import qs.Modules.Notifications.Sidebar.Chat
import qs.Components
import qs.Services
Item {
id: root
required property Item rootParent
required property Item fabRoot
property bool open: false
property int maxHeight: root.rootParent.height - Tokens.padding.medium * 2
property int openHeight: Math.min(modelsContent.item?.implicitHeight, maxHeight)
property int openWidth: root.rootParent.width - Tokens.padding.medium * 2
property real openHeightProg: openHeight / maxHeight
property int collapsedY: 0
property int horizontalContentMargin
property string header: qsTr("Available models")
property string acceptLabel: qsTr("Accept")
z: open || dialogTransition.running ? 2 : 0
Layout.fillWidth: true
implicitHeight: fabRoot.implicitHeight
function prettyModelName(): string {
if (modelsContent.item?.modelsList.count < 1)
return qsTr("No models found");
const pathList = LlmClient.model.split("\/");
const name = pathList[pathList.length - 1];
if (!name)
return qsTr("No model selected");
return name;
}
Behavior on implicitHeight {
Anim {}
}
function reparentWrapper(): void {
const newParent = open ? root.rootParent : root;
const c = modelsWrapper.parent.mapToItem(newParent, modelsWrapper.x + modelsWrapper.width / 2, modelsWrapper.y + modelsWrapper.height / 2);
root.collapsedY = modelsWrapper.y;
modelsWrapper.parent = newParent;
modelsWrapper.x = c.x - modelsWrapper.width / 2;
modelsWrapper.y = c.y - modelsWrapper.height / 2;
}
BlobGroup {
id: blobGroup
color: root.open ? Colors.palette.m3surfaceContainerHighest : Colors.tPalette.m3surfaceContainer
Behavior on color {
CAnim {}
}
}
MouseArea {
id: backdrop
anchors.fill: parent
enabled: false
hoverEnabled: enabled
preventStealing: true
parent: root.open ? root.rootParent : root
onClicked: root.open = false
}
Item {
id: modelsWrapper
width: root.width
height: modelBox.implicitHeight
z: 1
states: State {
name: "open"
when: root.open
PropertyChanges {
backdrop.enabled: true
modelsBackground.radius: Tokens.rounding.largeIncreased
modelsContent.opacity: 1
modelsWrapper.height: root.openHeight
modelsWrapper.width: root.openWidth
modelsWrapper.x: (root.rootParent.width - root.openWidth) / 2
modelsWrapper.y: root.rootParent.height - root.openHeight - Tokens.spacing.medium - Tokens.padding.medium - root.fabRoot.implicitHeight
elevation.opacity: 1
modelBox.opacity: 0
}
}
transitions: Transition {
id: dialogTransition
SequentialAnimation {
ScriptAction {
script: root.reparentWrapper()
}
Anim {
properties: "x,y"
}
}
PropertyAction {
property: "enabled"
}
Anim {
properties: "opacity,radius"
type: Anim.DefaultEffects
}
Anim {
properties: "width,height"
}
}
Elevation {
id: elevation
anchors.fill: parent
level: 4
opacity: 0
radius: modelsBackground.radius
transform: Matrix4x4 {
matrix: modelsBackground.deformMatrix
}
}
BlobRect {
id: modelsBackground
anchors.fill: parent
radius: modelLayer.pressed ? Tokens.rounding.small : Tokens.rounding.extraLarge
deformScale: 0
group: blobGroup
opacity: blobGroup.color.a * (root.enabled ? 1 : 0.5)
Behavior on radius {
Anim {
type: Anim.DefaultEffects
}
}
}
CustomRect {
id: modelBox
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
implicitHeight: root.fabRoot.implicitHeight
radius: modelsBackground.radius
CustomText {
id: label
text: root.prettyModelName()
color: Colors.palette.m3onSurfaceVariant
anchors.left: parent.left
anchors.leftMargin: Tokens.padding.large
anchors.verticalCenter: parent.verticalCenter
}
MaterialIcon {
color: Colors.palette.m3onSurfaceVariant
anchors.right: parent.right
anchors.rightMargin: Tokens.padding.large
anchors.verticalCenter: parent.verticalCenter
text: "unfold_more"
fontStyle: Tokens.font.icon.large
}
StateLayer {
id: modelLayer
onClicked: {
ChatState.fabExpanded = false;
root.open = true;
}
}
}
Loader {
id: modelsContent
active: opacity > 0
anchors.fill: parent
asynchronous: false
opacity: 0
sourceComponent: MouseArea {
readonly property alias modelsList: modelsList
onWheel: event => event.accepted = true
preventStealing: true
clip: true
implicitHeight: innerLayout.implicitHeight + innerLayout.anchors.margins * 2
ColumnLayout {
id: innerLayout
anchors.bottomMargin: Tokens.padding.largeIncreased
anchors.fill: parent
anchors.margins: Tokens.padding.extraLarge
spacing: 0
CustomText {
font: Tokens.font.title.builders.large.weight(Font.Normal).build()
text: root.header
}
VerticalFadeListView {
id: modelsList
Layout.fillWidth: true
Layout.leftMargin: root.horizontalContentMargin
Layout.topMargin: Tokens.spacing.medium
Layout.bottomMargin: Tokens.spacing.medium
Layout.rightMargin: root.horizontalContentMargin
Layout.preferredHeight: 400
Layout.maximumHeight: 400
model: LlmClient.availableModels
currentIndex: LlmClient.availableModels.indexOf(LlmClient.model)
visible: opacity > 0
Behavior on opacity {
Anim {}
}
CustomScrollBar.vertical: CustomScrollBar {
flickable: modelsList
}
delegate: CustomRect {
id: model
required property var modelData
required property int index
readonly property bool selected: ListView.view.currentIndex === model.index
implicitWidth: ListView.view.width
implicitHeight: name.implicitHeight + Tokens.padding.small * 2
radius: stateLayer.pressed ? Tokens.rounding.extraSmall : selected ? Tokens.rounding.largeIncreased : Tokens.rounding.medium
color: Qt.alpha(Colors.palette.m3tertiaryContainer, selected ? 1 : 0)
Behavior on radius {
Anim {
type: Anim.SlowEffects
}
}
RowLayout {
id: layout
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Tokens.padding.small
anchors.leftMargin: Tokens.padding.large
anchors.rightMargin: Tokens.padding.large
CustomText {
id: name
text: model.modelData
elide: CustomText.ElideRight
color: model.selected ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurface
Layout.fillWidth: true
}
MaterialIcon {
id: check
color: Colors.palette.m3onTertiaryContainer
fontStyle: Tokens.font.icon.medium
opacity: model.selected ? 1 : 0
visible: opacity > 0
text: "check"
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
Behavior on opacity {
Anim {
type: Anim.SlowEffects
}
}
}
}
StateLayer {
id: stateLayer
onClicked: {
model.ListView.view.currentIndex = model.index;
}
}
}
}
RowLayout {
Layout.alignment: Qt.AlignRight
spacing: Tokens.spacing.extraSmall
TextButton {
horizontalPadding: Tokens.padding.largeIncreased
isRound: true
text: qsTr("Cancel")
type: TextButton.Text
verticalPadding: Tokens.padding.small
stateLayer.preventStealing: true
onClicked: {
root.open = false;
}
}
TextButton {
horizontalPadding: Tokens.padding.largeIncreased
isRound: true
text: root.acceptLabel
type: TextButton.Text
stateLayer.preventStealing: true
verticalPadding: Tokens.padding.small
onClicked: {
LlmClient.selectModel(modelsList.currentItem.modelData);
root.open = false;
}
}
}
}
}
}
}
}
@@ -18,9 +18,9 @@ Item {
required property ChatGeneration current
readonly property bool isActive: index === blocks.length - 1
property bool expanded: false
readonly property LlmSegment lastSegment: root.segments[root.segments.length - 1]
readonly property var lastSegment: root.segments[root.segments.length - 1]
readonly property real totalElapsedMs: root.segments.reduce((sum, s) => sum + (s.elapsedMs ?? 0), 0)
readonly property bool pendingApproval: current.toolApprovalPending && segments.filter(s => s.type === LlmSegment.Type.ToolCall).some(s => current.pendingToolCalls.includes(s))
readonly property bool pendingApproval: current.toolApprovalPending && segments.some(s => s.type === LlmSegment.Type.ToolCall && s.status === LlmToolCall.Status.Pending)
implicitHeight: {
const expand = expandedRect.implicitHeight + layout.implicitHeight + expandedRect.anchors.topMargin;
@@ -91,6 +91,7 @@ Item {
id: collapsedText
color: Colors.palette.m3outline
font: Tokens.font.label.small
Layout.fillWidth: true
Layout.leftMargin: Tokens.spacing.medium
text: {
@@ -111,396 +112,17 @@ Item {
}
}
Item {
ApprovalPrompt {
id: approvalPrompt
anchors.top: layout.bottom
anchors.topMargin: Tokens.spacing.small
anchors.left: parent.left
anchors.right: parent.right
implicitHeight: approvalLayout.implicitHeight + approvalLayout.anchors.margins * 2
visible: root.pendingApproval
CustomRect {
id: approvalBg
anchors.fill: parent
radius: Tokens.rounding.medium
color: Colors.palette.m3surface
border.width: 1
border.color: Colors.palette.m3outlineVariant
ColumnLayout {
id: approvalLayout
anchors.fill: parent
anchors.margins: Tokens.padding.medium
spacing: Tokens.spacing.small
CustomText {
id: requestHeader
text: qsTr("Tool call request")
font: Tokens.font.headline.medium
color: Colors.palette.m3onSurface
}
Item {
id: toolsBg
property bool open: false
property Item rootParent: root.rootParent
property int maxHeight: rootParent.height - Tokens.padding.medium * 2
property int openHeight: Math.min(toolsContent.item?.implicitHeight, maxHeight)
property int openWidth: toolsBg.rootParent.width - Tokens.padding.medium * 2
property real openHeightProg: openHeight / maxHeight
property int collapsedY: 0
implicitHeight: toolList.implicitHeight + toolList.anchors.margins * 2
z: open || dialogTransition.running ? 2 : 0
Layout.fillWidth: true
function reparentWrapper(): void {
const newParent = open ? toolsBg.rootParent : toolsBg;
const c = toolsWrapper.parent.mapToItem(newParent, toolsWrapper.x + toolsWrapper.width / 2, toolsWrapper.y + toolsWrapper.height / 2);
toolsBg.collapsedY = toolsWrapper.y;
toolsWrapper.parent = newParent;
toolsWrapper.x = c.x - toolsWrapper.width / 2;
toolsWrapper.y = c.y - toolsWrapper.height / 2;
}
onOpenChanged: {
if (open)
toolsBg.rootParent.interactive = false;
else
toolsBg.rootParent.interactive = true;
}
BlobGroup {
id: blobGroup
color: toolsBg.open ? Colors.palette.m3surfaceContainerHighest : Colors.tPalette.m3primaryContainer
Behavior on color {
CAnim {}
}
}
MouseArea {
id: backdrop
anchors.fill: parent
enabled: false
hoverEnabled: enabled
preventStealing: true
parent: toolsBg.open ? toolsBg.rootParent : toolsBg
onClicked: toolsBg.open = false
}
Item {
id: toolsWrapper
width: toolsBg.width
height: toolBox.implicitHeight
rotation: parent === toolsBg.rootParent ? 180 : 0
z: 1
states: State {
name: "open"
when: toolsBg.open
PropertyChanges {
backdrop.enabled: true
toolsBackground.bottomLeftRadius: Tokens.rounding.largeIncreased
toolsBackground.bottomRightRadius: Tokens.rounding.largeIncreased
toolsBackground.topRightRadius: Tokens.rounding.largeIncreased
toolsBackground.topLeftRadius: Tokens.rounding.largeIncreased
toolsContent.opacity: 1
toolsWrapper.height: toolsBg.openHeight
toolsWrapper.width: toolsBg.openWidth
toolsWrapper.x: (toolsBg.rootParent.width - toolsBg.openWidth) / 2
toolsWrapper.y: (confirmationRow.implicitHeight + Tokens.spacing.small + Tokens.padding.medium) * (1 - toolsBg.openHeightProg) + Tokens.padding.medium
elevation.opacity: 1
toolBox.opacity: 0
}
}
transitions: Transition {
id: dialogTransition
SequentialAnimation {
ScriptAction {
script: toolsBg.reparentWrapper()
}
Anim {
properties: "x,y"
}
}
PropertyAction {
property: "enabled"
}
Anim {
properties: "opacity,topLeftRadius,topRightRadius,bottomLeftRadius,bottomRightRadius"
type: Anim.DefaultEffects
}
Anim {
properties: "width,height"
}
}
Elevation {
id: elevation
anchors.fill: parent
bottomLeftRadius: toolsBackground.bottomLeftRadius
bottomRightRadius: toolsBackground.bottomRightRadius
level: 4
opacity: 0
radius: toolsBackground.radius
transform: Matrix4x4 {
matrix: toolsBackground.deformMatrix
}
}
BlobRect {
id: toolsBackground
anchors.fill: parent
bottomLeftRadius: toolsBg.open ? Tokens.rounding.largeIncreased : Tokens.rounding.small
bottomRightRadius: toolsBg.open ? Tokens.rounding.largeIncreased : Tokens.rounding.small
topRightRadius: toolsBg.open ? Tokens.rounding.largeIncreased : Tokens.rounding.small
topLeftRadius: toolsBg.open ? Tokens.rounding.largeIncreased : Tokens.rounding.small
deformScale: 0
group: blobGroup
opacity: blobGroup.color.a * (root.enabled ? 1 : 0.5)
}
CustomRect {
id: toolBox
anchors.left: parent.left
anchors.right: parent.right
height: Math.min(implicitHeight, parent.height)
implicitHeight: toolList.implicitHeight + Tokens.padding.medium * 2
radius: Tokens.rounding.small
StateLayer {
onClicked: {
toolsBg.open = true;
}
}
ColumnLayout {
id: toolList
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Tokens.padding.medium
Repeater {
id: repeater
model: root.current.pendingToolCalls?.slice(0, 4) ?? []
delegate: RowLayout {
id: toolText
required property LlmSegment modelData
required property int index
spacing: Tokens.spacing.medium
function toolTarget(seg: LlmSegment): string {
var a = null;
try {
a = JSON.parse(seg.arguments);
} catch (e) {}
if (a && typeof a === "object") {
const v = a.url ?? a.path;
if (v !== undefined && v !== null)
return String(v);
}
return String(seg.arguments);
}
MaterialIcon {
color: Colors.palette.m3onPrimaryContainer
text: toolText.modelData.name === "webfetch" ? "web" : "text_snippet"
fill: 1
}
CustomText {
color: Colors.palette.m3onPrimaryContainer
text: qsTr("%1").arg(toolText.modelData.name)
}
MaterialIcon {
color: Colors.palette.m3onPrimaryContainer
text: "arrow_right_alt"
}
CustomText {
color: Colors.palette.m3onPrimaryContainer
text: qsTr("%1").arg(toolText.toolTarget(toolText.modelData))
}
}
}
CustomText {
color: Colors.palette.m3onPrimaryContainer
text: qsTr("And %1 more").arg(root.current.pendingToolCalls.slice(4).length)
visible: repeater.count < root.current.pendingToolCalls.length
}
}
}
Loader {
id: toolsContent
active: opacity > 0
anchors.fill: parent
asynchronous: false
opacity: 0
sourceComponent: MouseArea {
onWheel: event => event.accepted = true
preventStealing: true
implicitHeight: innerLayout.contentHeight + innerLayout.anchors.margins * 2
VerticalFadeListView {
id: innerLayout
anchors.fill: parent
anchors.margins: Tokens.padding.small
cacheBuffer: height * 10
fadeAmount: 0.1
fadeEnabled: interactive
spacing: 0
clip: true
interactive: contentHeight > toolsBg.maxHeight
model: root.current.pendingToolCalls ?? []
delegate: Item {
id: toolCall
required property LlmSegment modelData
property bool expanded: false
implicitWidth: CustomListView.view.width
implicitHeight: collapsedRow.implicitHeight + collapsedRow.anchors.margins + (expanded ? expandedTool.implicitHeight + expandedTool.anchors.margins + expandedTool.anchors.topMargin : collapsedRow.anchors.margins)
function toolTarget(seg: LlmSegment): string {
var a = null;
try {
a = JSON.parse(seg.arguments);
} catch (e) {}
if (a && typeof a === "object") {
const v = a.url ?? a.path;
if (v !== undefined && v !== null)
return String(v);
}
return String(seg.arguments);
}
Behavior on implicitHeight {
Anim {}
}
RowLayout {
id: collapsedRow
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Tokens.padding.medium
IconButton {
id: toolExpandBtn
font: Tokens.font.icon.medium
icon: "keyboard_arrow_down"
inactiveOnColor: hovered ? Colors.palette.m3onSurface : Colors.palette.m3outline
rotation: toolCall.expanded ? 180 : 0
type: IconButton.Text
Behavior on rotation {
Anim {}
}
onClicked: toolCall.expanded = !toolCall.expanded
}
CustomText {
id: preview
Layout.fillWidth: true
text: toolCall.modelData.name
}
}
CustomClippingRect {
id: expandedTool
anchors.top: collapsedRow.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: Tokens.padding.medium
anchors.topMargin: Tokens.spacing.medium
implicitHeight: fullToolCall.implicitHeight + fullToolCall.anchors.margins * 2
radius: Tokens.rounding.medium
color: Colors.palette.m3surfaceContainerLow
CustomText {
id: fullToolCall
anchors.fill: parent
anchors.margins: Tokens.padding.medium
text: toolCall.toolTarget(toolCall.modelData)
wrapMode: CustomText.WrapAtWordBoundaryOrAnywhere
}
}
}
}
}
}
}
}
ButtonRow {
id: confirmationRow
Layout.alignment: Qt.AlignRight
spacing: Tokens.spacing.medium
IconTextButton {
icon: "check_circle"
text: qsTr("Allow")
horizontalPadding: Tokens.padding.large
verticalPadding: Tokens.padding.small
isRound: true
shapeMorph: true
onClicked: root.current.approveTools()
}
IconTextButton {
icon: "cancel"
text: qsTr("Deny")
isRound: true
shapeMorph: true
inactiveColor: Colors.palette.m3error
inactiveOnColor: Colors.palette.m3onError
horizontalPadding: Tokens.padding.large
verticalPadding: Tokens.padding.small
onClicked: root.current.denyTools()
}
}
}
}
rootParent: root.rootParent
current: root.current
}
CustomClippingRect {
@@ -532,7 +154,7 @@ Item {
delegate: Item {
id: segment
required property LlmSegment modelData
required property var modelData
required property int index
implicitWidth: ListView.view.width
@@ -559,7 +181,7 @@ Item {
CustomText {
id: content
function toolTarget(seg: LlmSegment): string {
function toolTarget(seg: var): string {
var a = null;
try {
a = JSON.parse(seg.arguments);
@@ -572,29 +194,29 @@ Item {
return String(seg.arguments);
}
function prettyTool(seg: LlmSegment): string {
function prettyTool(seg: var): string {
if (seg.name === "webfetch") {
switch (seg.status) {
case LlmSegment.Status.Running:
case LlmToolCall.Status.Running:
return qsTr("Fetching");
case LlmSegment.Status.Pending:
case LlmToolCall.Status.Pending:
return qsTr("Pending fetch");
case LlmSegment.Status.Success:
case LlmToolCall.Status.Success:
return qsTr("Fetched");
case LlmSegment.Status.Error:
case LlmToolCall.Status.Error:
return qsTr("Failed to fetch");
default:
return qsTr("Fetched website");
}
} else if (seg.name === "readfile") {
switch (seg.status) {
case LlmSegment.Status.Running:
case LlmToolCall.Status.Running:
return qsTr("Reading");
case LlmSegment.Status.Pending:
case LlmToolCall.Status.Pending:
return qsTr("Pending read");
case LlmSegment.Status.Success:
case LlmToolCall.Status.Success:
return qsTr("Read");
case LlmSegment.Status.Error:
case LlmToolCall.Status.Error:
return qsTr("Failed to read");
default:
return qsTr("Read file");
@@ -608,6 +230,7 @@ Item {
anchors.topMargin: Tokens.spacing.small
anchors.right: parent.right
color: Colors.palette.m3outline
font: Tokens.font.body.small
wrapMode: CustomText.WrapAtWordBoundaryOrAnywhere
text: segment.modelData.type === LlmSegment.Type.Reasoning ? segment.modelData.text : qsTr("%1 %2").arg(content.prettyTool(segment.modelData)).arg(content.toolTarget(segment.modelData))
}
@@ -3,7 +3,7 @@ pragma Singleton
import Quickshell
import QtQuick
import ZShell.Config
import qs.Services
import qs.Helpers
import qs.Services
Singleton {
@@ -65,9 +65,7 @@ Item {
}
}
model: ScriptModel {
values: Chat.chats.values
}
model: LlmClient.chats
onLoadChatRequest: (chat, index) => {
ChatState.currentIdx = index;
@@ -75,17 +73,13 @@ Item {
}
onDeleteChatRequest: chat => {
if (chat === ChatState.chatSession) {
ChatState.chatSession = null;
ChatState.currentIdx = -1;
}
Chat.chats.remove(chat);
}
onNewChatRequest: {
const data = Chat.chats.insert();
ChatState.currentIdx = 0;
ChatState.chatSession = data;
const wasCurrent = chat === ChatState.chatSession;
LlmClient.chats.remove(chat);
if (wasCurrent)
ChatState.openDraft();
}
onNewChatRequest: ChatState.openDraft()
onRequestExpand: {
ChatState.narrowSidebarExpanded = true;
@@ -126,9 +120,8 @@ Item {
z: 0
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.margins: Tokens.padding.medium
anchors.right: parent.right
implicitWidth: root.width - leftMargin - sidebar.anchors.margins - Tokens.spacing.medium * 2 - anchors.margins * 2
implicitWidth: root.width - leftMargin - sidebar.anchors.margins - Tokens.spacing.medium * 2
states: State {
name: "wide"
@@ -151,40 +144,13 @@ Item {
ChatHost {
id: convHost
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
implicitWidth: Math.max(Math.min(parent.width, 800), Config.sidebar.sizes.width)
anchors.fill: parent
clip: true
onLoadingChanged: if (!loading)
root.chatOpen = true
}
ColumnLayout {
id: noChatLayout
anchors.centerIn: parent
opacity: ChatState.chatSession ? 0 : 1
visible: opacity > 0
Behavior on opacity {
Anim {}
}
MaterialIcon {
Layout.alignment: Qt.AlignCenter
text: "chat_bubble_off"
fontStyle: Tokens.font.icon.extraLarge
color: Colors.tPalette.m3onSurfaceVariant
}
CustomText {
Layout.alignment: Qt.AlignCenter
text: qsTr("Start or create a chat")
color: Colors.tPalette.m3onSurfaceVariant
}
}
MouseArea {
id: dimArea
anchors.fill: parent
@@ -196,8 +162,10 @@ Item {
const x = mouseX;
const margin = root.width - contentArea.implicitWidth - contentArea.anchors.margins * 2;
if (x > invertedRect.borderLeft - margin)
if (x > invertedRect.borderLeft - margin) {
ChatState.narrowSidebarExpanded = false;
ChatState.fabExpanded = false;
}
}
HoverHandler {
+57 -3
View File
@@ -1,6 +1,7 @@
pragma ComponentBehavior: Bound
import ZShell.Config
import ZShell.Llm
import QtQuick
import QtQuick.Layouts
import qs.Components
@@ -35,10 +36,10 @@ Item {
visible: height > 0
}
CustomClippingRect {
Item {
Layout.fillHeight: true
Layout.fillWidth: true
radius: Tokens.rounding.small
clip: true
Item {
id: pages
@@ -58,6 +59,7 @@ Item {
}
CustomRect {
id: bg
anchors.fill: parent
color: Colors.tPalette.m3surfaceContainerLow
radius: Tokens.rounding.small
@@ -96,11 +98,63 @@ Item {
CustomRect {
anchors.fill: parent
color: Colors.tPalette.m3surfaceContainerLow
radius: Tokens.rounding.small
radius: Tokens.rounding.large
ChatPanel {
anchors.fill: parent
}
GeminiGlow {
innerGlow: 50
z: 5
opacity: LlmClient.busy ? 1.0 : 0.0
glowSize: 0
visible: opacity > 0 && !ChatState.isWindow
layer.enabled: true
layer.effect: OpacityMask {
maskSource: fadeMask
}
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
}
Rectangle {
id: fadeMask
readonly property real fadeStartPos: {
const h = Math.max(1, height);
const nh = (h - h / 3) / h;
return nh;
}
anchors.fill: parent
layer.enabled: true
visible: false
gradient: Gradient {
orientation: Gradient.Vertical
GradientStop {
color: Qt.rgba(0, 0, 0, 0)
position: 0
}
GradientStop {
color: Qt.rgba(0, 0, 0, 0)
position: fadeMask.fadeStartPos
}
GradientStop {
color: Qt.rgba(0, 0, 0, 1.0)
position: 1.0
}
}
}
}
}
}
+2 -2
View File
@@ -35,7 +35,7 @@ Item {
}
}
onCurrentIndexChanged: root.screenState.sidebarTab = currentIndex
onCurrentIndexChanged: root.state.currentTab = currentIndex
Tab {
iconName: "notifications"
@@ -118,7 +118,7 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter
color: tab.current ? Colors.palette.m3primary : Colors.palette.m3onSurfaceVariant
fill: tab.current ? 1 : 0
fontStyle: Tokens.font.icon.medium
fontStyle: Tokens.font.icon.large
text: tab.iconName
Behavior on fill {
@@ -38,8 +38,8 @@ CustomRect {
return true;
});
}
readonly property int splitIndex: Math.ceil(quickToggles.length / 2)
required property ScreenState screenState
readonly property int splitIndex: Math.ceil(quickToggles.length / 2)
color: Colors.tPalette.m3surfaceContainer
implicitHeight: layout.implicitHeight + Tokens.padding.extraLargeIncreased
@@ -1,9 +1,9 @@
import Quickshell
import QtQuick
import QtQuick.Layouts
import ZShell.Config
import qs.Components
import qs.Modules.Notifications.Sidebar.Utils.Cards
import ZShell.Config
Item {
id: root
@@ -9,9 +9,9 @@ Item {
id: root
property real offsetScale: shouldBeActive ? 0 : 1
required property ScreenState screenState
readonly property bool shouldBeActive: screenState.sidebar && !sidebar.chatActive
required property Item sidebar
required property ScreenState screenState
anchors.bottomMargin: (-implicitHeight - 5) * offsetScale
implicitHeight: content.implicitHeight + Tokens.padding.small * 2
+4 -4
View File
@@ -11,11 +11,11 @@ import QtQuick
Item {
id: root
readonly property bool chatActive: screenState.sidebarTab === 1
property real offsetScale: shouldBeActive ? 0 : 1
required property var panels
readonly property bool shouldBeActive: root.screenState.sidebar && Config.sidebar.enabled
readonly property bool chatActive: screenState.sidebarTab === 1
required property ScreenState screenState
readonly property bool shouldBeActive: root.screenState.sidebar && Config.sidebar.enabled
anchors.rightMargin: (-implicitWidth - 5) * offsetScale
implicitWidth: Config.sidebar.sizes.width
@@ -30,9 +30,9 @@ Item {
}
Connections {
target: Chat
target: LlmClient
function onErrorOccurred(message: string): void {
function onGenerationFailed(session: ChatSession, message: string): void {
if (root.shouldBeActive && root.chatActive)
return;
+2 -1
View File
@@ -51,7 +51,8 @@ CustomRect {
value: Memory.percentage
Behavior on clampedVal {
Anim {}
Anim {
}
}
ColumnLayout {
+1 -1
View File
@@ -121,6 +121,6 @@ CustomRect {
activeIcon: "storage"
icon: modelData === Storage.primaryDisk ? "check" : ""
text: modelData.mount
text: modelData.name
}
}
+9
View File
@@ -0,0 +1,9 @@
import Quickshell
import QtQuick
import qs.Helpers
Scope {
Component.onCompleted: {
Wallpapers;
}
}
+1 -1
View File
@@ -17,7 +17,7 @@ CustomRect {
bottomLeftRadius: last ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
bottomRightRadius: last ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
color: Colors.tPalette.m3surfaceContainer
opacity: enabled && parent.enabled ? 1 : 0.5
opacity: enabled && (parent?.enabled ?? false) ? 1 : 0.5
topLeftRadius: first ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
topRightRadius: first ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
+18 -16
View File
@@ -10,24 +10,25 @@ import qs.Services
Item {
id: root
property bool acceptAllowed: true
required property string acceptLabel
required property Component content
required property string header
required property string label
required property Item rootParent
required property string settingAnchor
property bool acceptAllowed: true
property bool first: false
property int horizontalContentMargin
property string icon
required property string label
property alias iconLabel: openButton.iconLabel
property alias rowButton: openButton
property string subtext: ""
property bool last: false
property bool open
property real openHeight: Math.min(rootParent.height * 0.8, 600)
property real openWidth: Math.min(rootParent.width * 0.8, 400)
required property Item rootParent
property alias rowButton: openButton
property bool separateContent
required property string settingAnchor
property bool first: false
property bool last: false
signal accepted
signal cancelled
@@ -60,8 +61,8 @@ Item {
anchors.fill: parent
enabled: false
hoverEnabled: enabled
preventStealing: true
parent: root.open ? root.rootParent : root
preventStealing: true
onClicked: root.open = false
}
@@ -81,8 +82,8 @@ Item {
backdrop.enabled: true
dialogBg.bottomLeftRadius: Tokens.rounding.largeIncreased
dialogBg.bottomRightRadius: Tokens.rounding.largeIncreased
dialogBg.topRightRadius: Tokens.rounding.largeIncreased
dialogBg.topLeftRadius: Tokens.rounding.largeIncreased
dialogBg.topRightRadius: Tokens.rounding.largeIncreased
dialogContent.opacity: 1
dialogWrapper.height: root.openHeight
dialogWrapper.width: root.openWidth
@@ -140,11 +141,11 @@ Item {
anchors.fill: parent
bottomLeftRadius: root.last ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
bottomRightRadius: root.last ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
topRightRadius: root.first ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
topLeftRadius: root.first ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
deformScale: 0
group: blobGroup
opacity: blobGroup.color.a * (root.enabled ? 1 : 0.5)
topLeftRadius: root.first ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
topRightRadius: root.first ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
}
RowButton {
@@ -153,11 +154,11 @@ Item {
anchors.left: parent.left
anchors.right: parent.right
color: "transparent"
first: root.first
height: Math.min(implicitHeight, parent.height)
icon: root.icon
subtext: root.subtext
last: root.last
first: root.first
subtext: root.subtext
text: root.label
transform: Matrix4x4 {
@@ -176,9 +177,10 @@ Item {
opacity: 0
sourceComponent: MouseArea {
onWheel: event => event.accepted = true
preventStealing: true
onWheel: event => event.accepted = true
ColumnLayout {
anchors.bottomMargin: Tokens.padding.largeIncreased
anchors.fill: parent
@@ -227,10 +229,10 @@ Item {
TextButton {
horizontalPadding: Tokens.padding.largeIncreased
isRound: true
stateLayer.preventStealing: true
text: qsTr("Cancel")
type: TextButton.Text
verticalPadding: Tokens.padding.small
stateLayer.preventStealing: true
onClicked: {
root.cancelled();
@@ -242,9 +244,9 @@ Item {
enabled: root.acceptAllowed
horizontalPadding: Tokens.padding.largeIncreased
isRound: true
stateLayer.preventStealing: true
text: root.acceptLabel
type: TextButton.Text
stateLayer.preventStealing: true
verticalPadding: Tokens.padding.small
onClicked: {
@@ -9,8 +9,9 @@ DialogRowButton {
id: root
required property var model
property var selectedItem
property var initialSelect
property var selectedItem
function keyFor(item: var): string {
return item.id;
@@ -178,6 +178,9 @@ Item {
sourceComponent: MouseArea {
onWheel: event => event.accepted = true
preventStealing: true
acceptedButtons: Qt.NoButton
cursorShape: undefined
enabled: dialogContent.opacity > 0
clip: true
implicitWidth: innerLayout.implicitWidth + innerLayout.anchors.margins * 2
implicitHeight: innerLayout.implicitHeight + innerLayout.anchors.margins + innerLayout.anchors.bottomMargin
+2 -2
View File
@@ -16,7 +16,7 @@ ConnectedRect {
property string iconText: "open_in_new"
property int verticalPadding: Tokens.padding.small
signal clicked(event: MouseEvent)
signal clicked
signal toggled(checked: bool)
Layout.fillWidth: true
@@ -56,7 +56,7 @@ ConnectedRect {
}
StateLayer {
onClicked: e => root.clicked(e)
onClicked: root.clicked()
}
CustomSwitch {
+74 -80
View File
@@ -13,9 +13,7 @@ import qs.Services
Item {
id: wrapper
readonly property var currentScreen: screens.length > selectedScreenIndex ? screens[selectedScreenIndex] : null
readonly property real maxZoom: 5.0
property int buttonRowHeight: Tokens.padding.large * 3
readonly property bool changesMade: {
cropRevision;
@@ -27,21 +25,13 @@ 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;
}
property int cropRevision: 0
property int buttonRowHeight: Tokens.padding.large * 3
readonly property var currentScreen: screens.length > selectedScreenIndex ? screens[selectedScreenIndex] : null
readonly property real maxZoom: 5.0
property var screens: []
property int selectedScreenIndex: 0
property bool shouldBeActive: true
function zoomFromSlider(t: real): real {
return 1 / (1 - t * (1 - 1 / maxZoom));
}
function sliderFromZoom(zoom: real): real {
return (1 - 1 / zoom) / (1 - 1 / maxZoom);
}
function applyCrop(): void {
if (!cropRectLoader.item || !currentScreen)
return;
@@ -72,6 +62,10 @@ Item {
Qt.callLater(syncCropToScreen);
}
function sliderFromZoom(zoom: real): real {
return (1 - 1 / zoom) / (1 - 1 / maxZoom);
}
function syncCropToScreen(): void {
if (cropRectLoader.item)
cropRectLoader.item.restoreFromData();
@@ -94,6 +88,10 @@ Item {
cropRect.clampToBounds();
}
function zoomFromSlider(t: real): real {
return 1 / (1 - t * (1 - 1 / maxZoom));
}
anchors.left: parent.left
anchors.right: parent.right
implicitHeight: shouldBeActive ? 430 : 0
@@ -102,13 +100,16 @@ Item {
visible: opacity > 0
Behavior on opacity {
Anim {}
Anim {
}
}
Behavior on scale {
Anim {}
Anim {
}
}
Behavior on y {
Anim {}
Anim {
}
}
Component.onCompleted: {
@@ -140,12 +141,12 @@ Item {
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
font: Tokens.font.body.builders.large.weight(Font.DemiBold).build()
onClicked: wrapper.selectScreen(index)
}
@@ -180,15 +181,15 @@ Item {
spacing: Tokens.spacing.medium
IconTextButton {
isRound: true
icon: "close"
text: qsTr("Cancel")
enabled: wrapper.changesMade
font: Tokens.font.body.large
shapeMorph: true
horizontalPadding: Tokens.padding.large
icon: "close"
inactiveColor: Colors.layer(Colors.palette.m3surfaceContainerHighest, 2)
inactiveOnColor: Colors.palette.m3onSurfaceVariant
enabled: wrapper.changesMade
isRound: true
shapeMorph: true
text: qsTr("Cancel")
onClicked: {
wrapper.syncCropToScreen();
@@ -196,15 +197,15 @@ Item {
}
IconTextButton {
isRound: true
icon: "check"
text: qsTr("Apply")
enabled: wrapper.changesMade
font: Tokens.font.body.large
shapeMorph: true
horizontalPadding: Tokens.padding.large
icon: "check"
inactiveColor: Colors.palette.m3secondary
inactiveOnColor: Colors.palette.m3onSecondary
enabled: wrapper.changesMade
isRound: true
shapeMorph: true
text: qsTr("Apply")
onClicked: {
wrapper.applyCrop();
@@ -227,42 +228,37 @@ Item {
id: fitBox
readonly property real aspect: scaledImg.implicitHeight > 0 ? scaledImg.implicitWidth / scaledImg.implicitHeight : 1
readonly property real fitW: Math.min(imageSlot.width, imageSlot.height * aspect)
readonly property real fitW: imageSlot.height * aspect
anchors.centerIn: parent
implicitHeight: Math.round(fitW / aspect)
implicitWidth: Math.round(fitW)
x: Math.round((imageSlot.width - width) / 2)
y: Math.round((imageSlot.height - height) / 2)
radius: Tokens.rounding.largeIncreased
Loader {
anchors.centerIn: parent
opacity: scaledImg.status === Image.Ready ? 0 : 1
active: opacity > 0
anchors.centerIn: parent
opacity: scaledImg.status === Image.Ready ? 0 : 1
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
sourceComponent: CustomRect {
implicitWidth: loadingIndicator.implicitSize + Tokens.padding.large * 2
implicitHeight: loadingIndicator.implicitSize + Tokens.padding.large * 2
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(fitBox.width, fitBox.height) * 0.3
}
}
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
}
Image {
@@ -270,14 +266,16 @@ Item {
anchors.fill: parent
asynchronous: true
fillMode: Image.Stretch
source: Wallpapers.current
sourceSize: {
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
return Qt.size(width * dpr, height * dpr);
}
retainWhileLoading: true
fillMode: Image.PreserveAspectFit
mipmap: true
opacity: status === Image.Ready ? 1 : 0
retainWhileLoading: true
smooth: true
source: Wallpapers.current
sourceSize.height: {
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
return imageSlot.height * dpr;
}
Behavior on opacity {
Anim {
@@ -297,7 +295,6 @@ Item {
property real aspectRatio: wrapper.currentScreen ? wrapper.currentScreen.width / wrapper.currentScreen.height : 1
readonly property real baseHeight: baseWidth / aspectRatio
readonly property real baseWidth: {
let fittedHeight = imageH;
let fittedWidth = fittedHeight * aspectRatio;
@@ -309,7 +306,6 @@ Item {
return fittedWidth;
}
readonly property real imageH: Math.round(fitBox.height)
readonly property real imageW: Math.round(fitBox.width)
readonly property real imageX: 0
@@ -327,6 +323,13 @@ Item {
y = Math.max(imageY, Math.min(y, imageY + imageH - height));
}
function restoreFromData() {
const t = savedTarget();
zoom = t.zoom;
x = t.x;
y = t.y;
}
// Where the rect should be according to saved data (same rules as a restore).
function savedTarget() {
const data = Wallpapers.getCrop(wrapper.currentScreen.name);
@@ -349,19 +352,13 @@ Item {
};
}
function restoreFromData() {
const t = savedTarget();
zoom = t.zoom;
x = t.x;
y = t.y;
}
color: zoomSlider.dragging || mouse.pressed ? Qt.alpha(Colors.palette.m3tertiary, 0.3) : "transparent"
height: Math.round(baseHeight / zoom)
width: Math.round(baseWidth / zoom)
color: zoomSlider.dragging || mouse.pressed ? Qt.alpha(Colors.palette.m3tertiary, 0.3) : "transparent"
Behavior on color {
CAnim {}
CAnim {
}
}
Component.onCompleted: {
@@ -374,21 +371,19 @@ Item {
}
ShaderEffect {
anchors.fill: parent
property color borderColor: Colors.palette.m3tertiary
property real borderWidth: 2
property real cornerRadius: fitBox.radius
property vector4d cropBox: cropRectLoader.item ? Qt.vector4d(snap(cropRectLoader.item.x), snap(cropRectLoader.item.y), snap(cropRectLoader.item.width), snap(cropRectLoader.item.height)) : Qt.vector4d(0, 0, 0, 0)
readonly property real dpr: (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1
property vector2d itemSize: Qt.vector2d(width, height)
property real pixelScale: dpr
function snap(v: real): real {
return Math.round(v * dpr) / dpr;
}
property color borderColor: Colors.palette.m3tertiary
property vector4d cropBox: cropRectLoader.item ? Qt.vector4d(snap(cropRectLoader.item.x), snap(cropRectLoader.item.y), snap(cropRectLoader.item.width), snap(cropRectLoader.item.height)) : Qt.vector4d(0, 0, 0, 0)
property real borderWidth: 2
property real cornerRadius: fitBox.radius
property vector2d itemSize: Qt.vector2d(width, height)
property real pixelScale: dpr
anchors.fill: parent
fragmentShader: Qt.resolvedUrl(`${Quickshell.shellDir}/assets/shaders/crop_sdf.frag.qsb`)
}
@@ -398,14 +393,6 @@ Item {
property real grabX: 0
property real grabY: 0
function insideCropRect(mx, my): bool {
const r = cropRectLoader.item;
if (!r)
return false;
return mx >= r.x && mx <= r.x + r.width && my >= r.y && my <= r.y + r.height;
}
function beginGrab(mx, my): void {
const r = cropRectLoader.item;
if (!r)
@@ -422,6 +409,14 @@ Item {
}
}
function insideCropRect(mx, my): bool {
const r = cropRectLoader.item;
if (!r)
return false;
return mx >= r.x && mx <= r.x + r.width && my >= r.y && my <= r.y + r.height;
}
function updateCrop(mx, my): void {
if (!cropRectLoader.item)
return;
@@ -439,11 +434,10 @@ Item {
}
anchors.fill: parent
cursorShape: pressed ? Qt.ClosedHandCursor : insideCropRect(mouseX, mouseY) ? Qt.OpenHandCursor : Qt.PointingHandCursor
hoverEnabled: true
preventStealing: true
cursorShape: pressed ? Qt.ClosedHandCursor : insideCropRect(mouseX, mouseY) ? Qt.OpenHandCursor : Qt.PointingHandCursor
onPositionChanged: mouse => {
if (pressed) {
updateCrop(mouse.x, mouse.y);
-12
View File
@@ -42,18 +42,6 @@ Item {
objectName: "PageContainer"
Component.onCompleted: root.loadPage(root.sState.currentPageIdx)
// CustomRect {
// anchors.fill: parent
// color: Qt.alpha(Colors.palette.m3shadow, 0.3)
// opacity: root.sState.dimmed ? 1 : 0
// visible: opacity > 0
// z: 0
//
// Behavior on opacity {
// Anim {}
// }
// }
}
Connections {
+2 -2
View File
@@ -52,11 +52,11 @@ PageBase {
anchors.leftMargin: Tokens.padding.largeIncreased
anchors.margins: Tokens.padding.small
anchors.rightMargin: Tokens.padding.largeIncreased
spacing: Tokens.spacing.small
spacing: Tokens.spacing.medium
IconImage {
asynchronous: true
implicitSize: Math.round(Tokens.font.icon.large * 1.8)
implicitSize: Math.round(Tokens.font.icon.large.pointSize * 1.8)
source: Quickshell.iconPath(appItem.modelData.icon, "image-missing")
}
@@ -55,10 +55,6 @@ PageBase {
enabled: Object.keys(model).length > 0
header: qsTr("Add new entry")
icon: "add"
settingAnchor: "panels-bar-status-icons-add-item"
onOpenChanged: {
root.sState.dimmed = open;
}
last: true
label: qsTr("Add entry")
model: {
@@ -69,6 +65,7 @@ PageBase {
}));
}
rootParent: root.flickable
settingAnchor: "panels-bar-status-icons-add-item"
onAccepted: {
if (!selectedItem)
@@ -88,19 +88,12 @@ PageBase {
id: selectScheme
acceptLabel: qsTr("Confirm")
settingAnchor: "panels-sidebar-llm-scheme"
enabled: Object.keys(model).length > 0
settingAnchor: "panels-sidebar-select-scheme"
separateContent: false
header: qsTr("Select scheme")
first: true
last: true
icon: "format_paint"
iconLabel.fontStyle: Tokens.font.icon.large
rowButton.trailingIcon: "open_in_new"
onOpenChanged: {
root.sState.dimmed = open;
}
rowButton.activeItem: root.schemes.find(s => s.id === Config.llm.appearance.scheme).label
icon: "add"
label: qsTr("Color scheme")
subtext: qsTr("Select the color scheme used for code blocks")
model: root.schemes
+2 -14
View File
@@ -25,18 +25,6 @@ PageBase {
text: qsTr("None")
}
]
readonly property list<string> gpuValues: ["", "NVIDIA", "GENERIC", "None"]
function gpuKeyToIndex(key: string): int {
const u = (key ?? "").trim().toUpperCase();
if (u === "")
return 0; // Auto
if (u === "NVIDIA")
return 1;
if (u === "GENERIC")
return 2;
return 3; // None
}
title: qsTr("Services")
@@ -211,7 +199,7 @@ PageBase {
}
SelectRow {
active: root.gpuItems[root.gpuKeyToIndex(Config.services.gpuType)]
active: root.gpuItems[Config.services.gpuType]
last: true
menuItems: root.gpuItems
menuOnTop: true
@@ -219,7 +207,7 @@ PageBase {
subtext: Gpu.name ? qsTr("Monitoring: %1").arg(Gpu.name) : qsTr("Override for GPU type")
text: qsTr("GPU")
onSelected: item => Config.services.gpuType = root.gpuValues[root.gpuItems.indexOf(item)]
onSelected: item => Config.services.gpuType = root.gpuItems.indexOf(item)
}
}
}
-6
View File
@@ -38,9 +38,6 @@ PageBase {
spacing: Tokens.spacing.small
IconTextButton {
property string settingAnchor: "style-wallpapers"
enabled: Config.background.enabled
horizontalPadding: Tokens.padding.extraLarge
icon: "wallpaper"
@@ -54,9 +51,6 @@ PageBase {
}
IconTextButton {
property string settingAnchor: "style-colors-fonts"
enabled: Config.background.enabled
horizontalPadding: Tokens.padding.extraLarge
icon: "palette"
+17
View File
@@ -11,6 +11,18 @@ Scope {
readonly property bool hasFullscreen: Hypr.focusedWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen === 2) ?? false
property bool launcherInterrupted
CustomShortcut {
description: "Open screenshot"
name: "screenshot"
onPressed: {
const states = ShellState.allStates();
for (const s of states) {
s.screenshot = true;
}
}
}
CustomShortcut {
description: "Toggle launcher"
name: "toggle-launcher"
@@ -26,6 +38,7 @@ Scope {
}
CustomShortcut {
description: "Toggle drawing"
name: "toggle-drawing"
onPressed: {
@@ -35,6 +48,7 @@ Scope {
}
CustomShortcut {
description: "Toggle sidebar"
name: "toggle-nc"
onPressed: {
@@ -44,6 +58,7 @@ Scope {
}
CustomShortcut {
description: "Show On-Screen-Display"
name: "show-osd"
onPressed: {
@@ -53,6 +68,7 @@ Scope {
}
CustomShortcut {
description: "Toggle settings"
name: "toggle-settings"
onPressed: {
@@ -62,6 +78,7 @@ Scope {
}
CustomShortcut {
description: "Toggle clipboard"
name: "toggle-clipboard"
onPressed: {
-51
View File
@@ -1,51 +0,0 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Hyprland
import QtQuick
import qs.Components
import qs.Helpers
import ZShell.Internal
Item {
id: root
readonly property var displayData: Wallpapers.getCrop(screen.name)
required property ShellScreen screen
property size screenResolution: Qt.size(screen.width * screenScale, screen.height * screenScale)
property real screenScale: Hyprland.monitorFor(screen).scale
anchors.fill: parent
Component.onCompleted: Hyprland.refreshMonitors()
WallpaperImage {
id: img
anchors.fill: parent
cropHeight: root.displayData?.height ?? 1.0
cropWidth: root.displayData?.width ?? 1.0
cropX: root.displayData?.x ?? 0.0
cropY: root.displayData?.y ?? 0.0
fadeDuration: 150
screenResolution: root.screenResolution
source: Wallpapers.current
Behavior on cropHeight {
Anim {
}
}
Behavior on cropWidth {
Anim {
}
}
Behavior on cropX {
Anim {
}
}
Behavior on cropY {
Anim {
}
}
}
}
-47
View File
@@ -1,47 +0,0 @@
import Quickshell
import QtQuick
import Quickshell.Wayland
import ZShell.Config
import qs.Modules.DesktopIcons
Loader {
active: Config.background.enabled
asynchronous: false
sourceComponent: Variants {
model: Quickshell.screens
PanelWindow {
id: root
required property var modelData
WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.layer: WlrLayer.Background
WlrLayershell.namespace: "ZShell-Wallpaper"
color: "transparent"
screen: modelData
anchors {
bottom: true
left: true
right: true
top: true
}
WallBackground {
screen: root.screen
}
Loader {
id: loader
active: Config.general.desktopIcons
anchors.fill: parent
sourceComponent: DesktopIcons {
}
}
}
}
}
+22 -1
View File
@@ -1,5 +1,25 @@
find_package(Qt6 REQUIRED COMPONENTS ShaderTools Core Qml Gui Quick QuickPrivate Concurrent Sql Network DBus CanvasPainter)
find_package(Qt6 REQUIRED COMPONENTS
ShaderTools
Core
Qml
Gui
GuiPrivate
Quick
CanvasPainter
QuickPrivate
Concurrent
Sql
Network
DBus
CanvasPainter
WaylandClient
WaylandClientPrivate
)
find_package(PkgConfig REQUIRED)
pkg_check_modules(WAYLAND_CLIENT REQUIRED IMPORTED_TARGET wayland-client)
pkg_check_modules(EGL REQUIRED IMPORTED_TARGET egl)
pkg_check_modules(WAYLAND_EGL REQUIRED IMPORTED_TARGET wayland-egl)
pkg_check_modules(GL REQUIRED IMPORTED_TARGET gl)
pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED)
pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED)
pkg_check_modules(Aubio IMPORTED_TARGET aubio REQUIRED)
@@ -17,6 +37,7 @@ qt_standard_project_setup(REQUIRES 6.9)
include(cmake/pch.cmake)
include(cmake/qml-module.cmake)
include(cmake/wayland-protocol.cmake)
add_library(zshell-util INTERFACE)
target_include_directories(zshell-util INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/include")
+17 -13
View File
@@ -1,20 +1,24 @@
qml_module(ZShell-blobs
URI ZShell.Blobs
SOURCES
blobgroup.cpp
blobshape.cpp
blobrect.cpp
blobinvertedrect.cpp
blobmaterial.cpp
LIBRARIES
Qt::Quick
SOURCES
blobgroup.cpp
blobshape.cpp
blobrect.cpp
blobinvertedrect.cpp
blobgroup2.cpp
blobshape2.cpp
blobrect2.cpp
blobinvertedrect2.cpp
blobmaterial.cpp
LIBRARIES
Qt::Quick
)
qt_add_shaders(ZShell-blobs "blob_shaders"
BATCHABLE OPTIMIZED NOHLSL NOMSL
BATCHABLE OPTIMIZED NOHLSL NOMSL
GLSL "300es,330"
PREFIX "/"
FILES
shaders/blob.frag
shaders/blob.vert
PREFIX "/"
FILES
shaders/blob.frag
shaders/blob.vert
)
+387
View File
@@ -0,0 +1,387 @@
#include "blobgroup2.hpp"
#include "blobinvertedrect2.hpp"
#include "blobshape2.hpp"
#include <qsggeometry.h>
#include <qsgnode.h>
#include <algorithm>
#include <cmath>
#include <cstring>
static float cpuSdBox(
float px, float py, float cx, float cy, float hw, float hh) {
const float dx = std::abs(px - cx) - hw;
const float dy = std::abs(py - cy) - hh;
const float mdx = std::max(dx, 0.0f);
const float mdy = std::max(dy, 0.0f);
return std::sqrt(mdx * mdx + mdy * mdy) + std::min(std::max(dx, dy), 0.0f);
}
static float cpuSmoothstep(float edge0, float edge1, float x) {
const float t = std::clamp((x - edge0) / (edge1 - edge0), 0.0f, 1.0f);
return t * t * (3.0f - 2.0f * t);
}
static float cornerFillFactor(float sd, float smoothFactor) {
const float outside = cpuSmoothstep(0.0f, smoothFactor, sd);
const float inside = cpuSmoothstep(0.0f, -smoothFactor, sd);
return std::max(outside, inside);
}
BlobGroup2::BlobGroup2(QQuickItem* parent) : QQuickItem(parent) {
setFlag(ItemHasContents);
}
BlobGroup2::~BlobGroup2() {
for (auto* shape : std::as_const(m_shapes))
shape->m_group = nullptr;
if (m_invertedRect)
static_cast<BlobShape2*>(m_invertedRect)->m_group = nullptr;
}
void BlobGroup2::setSmoothing(qreal s) {
if (qFuzzyCompare(m_smoothing, s)) return;
m_smoothing = s;
emit smoothingChanged();
markDirty();
}
void BlobGroup2::setColor(const QColor& c) {
if (m_color == c) return;
m_color = c;
emit colorChanged();
markDirty();
}
void BlobGroup2::setCornerFill(bool e) {
if (m_cornerFill == e) return;
m_cornerFill = e;
emit cornerFillChanged();
markDirty();
}
void BlobGroup2::setShadowColor(const QColor& c) {
if (m_shadowColor == c) return;
m_shadowColor = c;
emit shadowColorChanged();
markDirty();
}
void BlobGroup2::setShadowRadius(qreal r) {
if (qFuzzyCompare(m_shadowRadius, r)) return;
m_shadowRadius = r;
emit shadowRadiusChanged();
markDirty();
}
void BlobGroup2::addShape(BlobShape2* shape) {
if (!shape || m_shapes.contains(shape)) return;
m_shapes.append(shape);
markDirty();
}
void BlobGroup2::removeShape(BlobShape2* shape) {
m_shapes.removeOne(shape);
markDirty();
}
void BlobGroup2::setInvertedRect(BlobInvertedRect2* rect) {
if (m_invertedRect == rect) return;
m_invertedRect = rect;
markDirty();
}
void BlobGroup2::clearInvertedRect(BlobInvertedRect2* rect) {
if (m_invertedRect != rect) return;
m_invertedRect = nullptr;
markDirty();
}
void BlobGroup2::markDirty() {
m_physicsUpdated = false;
m_dataBuilt = false;
// Physics runs during polish, before the render pass. The panel content is
// transformed by deformMatrix from QML, so the matrix has to be final for this
// frame; building the data in updatePaintNode would let the transform lag one
// frame behind the material.
polish();
update();
}
void BlobGroup2::updatePolish() {
// The group is the only thing that draws, so it also drives the shapes'
// physics.
ensureDataBuilt();
}
void BlobGroup2::ensurePhysicsUpdated() {
if (m_physicsUpdated) return;
m_physicsUpdated = true;
for (auto* shape : std::as_const(m_shapes))
shape->updatePhysics();
}
void BlobGroup2::ensureDataBuilt() {
if (m_dataBuilt) return;
ensurePhysicsUpdated();
// Physics can request the next frame through markDirty(); the data is
// refreshed here anyway, so the flag is set after it runs.
m_dataBuilt = true;
m_data.rects.clear();
m_bounds = QRectF();
const float smoothFactor = static_cast<float>(m_smoothing);
// The same reach the shader uses to decide whether a rect can contribute, plus
// the shadow, so every pixel the shader can give a value to is inside the node.
const double extent = static_cast<double>(smoothFactor) * 1.5 +
static_cast<double>(m_shadowRadius);
// Group order, no pruning by proximity: the shader sees this list for every pixel. A
// shape that is not pushing is not in the list at all: shapes here are data for the
// group draw, so leaving a panel at rest out means it cannot define any part of the
// edge. A closing panel is still in the list while its curve runs out, even with no
// size left, which is what keeps the bounce.
// entryShapes maps a shared entry back to the shape it came from.
QVector<BlobShape2*> entryShapes;
for (auto* shape : std::as_const(m_shapes)) {
if (!shape->isMoving()) continue;
if (shape->width() <= 0 || shape->height() <= 0) continue;
if (static_cast<int>(m_data.rects.size()) >= BlobGroupData::kMaxRects)
continue;
const QPointF scene = shape->mapToScene(QPointF(0, 0));
const QMatrix4x4& dm = shape->m_deformMatrix;
BlobRectData r;
r.cx = static_cast<float>(scene.x() + shape->width() / 2.0);
r.cy = static_cast<float>(scene.y() + shape->height() / 2.0);
r.hw = static_cast<float>(shape->width()) * 0.5f;
r.hh = static_cast<float>(shape->height()) * 0.5f;
shape->cornerRadii(r.radius);
r.offsetX = dm(0, 3);
r.offsetY = dm(1, 3);
const float a = dm(0, 0), b = dm(1, 0);
const float c = dm(0, 1), e = dm(1, 1);
const float det = a * e - c * b;
const float invDet = std::abs(det) > 1e-6f ? 1.0f / det : 1.0f;
r.invDeform[0] = e * invDet;
r.invDeform[1] = -b * invDet;
r.invDeform[2] = -c * invDet;
r.invDeform[3] = a * invDet;
const float halfTr = 0.5f * (a + e);
const float halfDiff = 0.5f * (a - e);
r.minEig = halfTr - std::sqrt(halfDiff * halfDiff + c * c);
r.screenHalfX = std::abs(a) * r.hw + std::abs(c) * r.hh;
r.screenHalfY = std::abs(b) * r.hw + std::abs(e) * r.hh;
m_data.rects.append(r);
m_bounds = m_bounds.united(QRectF(
static_cast<double>(r.cx) + static_cast<double>(r.offsetX) -
static_cast<double>(r.screenHalfX) - extent,
static_cast<double>(r.cy) + static_cast<double>(r.offsetY) -
static_cast<double>(r.screenHalfY) - extent,
2.0 * (static_cast<double>(r.screenHalfX) + extent),
2.0 * (static_cast<double>(r.screenHalfY) + extent)));
entryShapes.append(shape);
}
const int count = static_cast<int>(m_data.rects.size());
// Exclusion gates only the smooth blend.
for (int i = 0; i < count; ++i) {
int mask = 0;
BlobShape2* const si = entryShapes[i];
for (int j = 0; j < count; ++j) {
if (j == i) continue;
BlobShape2* const sj = entryShapes[j];
if (si->isExcluded(sj) || sj->isExcluded(si)) mask |= (1 << j);
}
m_data.rects[i].excludeMask = mask;
}
constexpr float minR = 2.0f;
const bool cornerFill = m_cornerFill;
for (int i = 0; i < count; ++i) {
auto& ri = m_data.rects[i];
BlobShape2* const si = entryShapes[i];
float fTr = 1.0f, fBr = 1.0f, fBl = 1.0f, fTl = 1.0f;
const float cTrX = ri.cx + ri.hw, cTrY = ri.cy - ri.hh;
const float cBrX = ri.cx + ri.hw, cBrY = ri.cy + ri.hh;
const float cBlX = ri.cx - ri.hw, cBlY = ri.cy + ri.hh;
const float cTlX = ri.cx - ri.hw, cTlY = ri.cy - ri.hh;
if (cornerFill) {
for (int j = 0; j < count; ++j) {
if (j == i) continue;
if (ri.excludeMask & (1 << j)) continue;
BlobShape2* const sj = entryShapes[j];
if (si->isCornerExcluded(sj) || sj->isCornerExcluded(si))
continue;
const auto& rj = m_data.rects[j];
fTr = std::min(
fTr,
cornerFillFactor(
cpuSdBox(cTrX, cTrY, rj.cx, rj.cy, rj.hw, rj.hh),
smoothFactor));
fBr = std::min(
fBr,
cornerFillFactor(
cpuSdBox(cBrX, cBrY, rj.cx, rj.cy, rj.hw, rj.hh),
smoothFactor));
fBl = std::min(
fBl,
cornerFillFactor(
cpuSdBox(cBlX, cBlY, rj.cx, rj.cy, rj.hw, rj.hh),
smoothFactor));
fTl = std::min(
fTl,
cornerFillFactor(
cpuSdBox(cTlX, cTlY, rj.cx, rj.cy, rj.hw, rj.hh),
smoothFactor));
}
}
ri.radius[0] = std::max(ri.radius[0] * fTr, minR);
ri.radius[1] = std::max(ri.radius[1] * fBr, minR);
ri.radius[2] = std::max(ri.radius[2] * fBl, minR);
ri.radius[3] = std::max(ri.radius[3] * fTl, minR);
}
// Inverted rect. The frame term is evaluated for every pixel in the bounds,
// so the group flag cannot be gated per shape.
if (!m_invertedRect) return;
auto* inv = static_cast<BlobShape2*>(m_invertedRect);
if (inv->width() <= 0 || inv->height() <= 0) return;
const QPointF invScene = inv->mapToScene(QPointF(0, 0));
const float outerCX = static_cast<float>(invScene.x() + inv->width() / 2.0);
const float outerCY =
static_cast<float>(invScene.y() + inv->height() / 2.0);
const float outerHW = static_cast<float>(inv->width()) * 0.5f;
const float outerHH = static_cast<float>(inv->height()) * 0.5f;
const auto* invRect = m_invertedRect;
const float innerCX =
outerCX + static_cast<float>(
(invRect->borderLeft() - invRect->borderRight()) / 2.0);
const float innerCY =
outerCY + static_cast<float>(
(invRect->borderTop() - invRect->borderBottom()) / 2.0);
const float innerHW =
outerHW - static_cast<float>(
(invRect->borderLeft() + invRect->borderRight()) / 2.0);
const float innerHH =
outerHH - static_cast<float>(
(invRect->borderTop() + invRect->borderBottom()) / 2.0);
m_data.hasInverted = true;
m_data.invertedRadius = static_cast<float>(invRect->radius());
m_data.invertedOuter[0] = outerCX;
m_data.invertedOuter[1] = outerCY;
m_data.invertedOuter[2] = outerHW;
m_data.invertedOuter[3] = outerHH;
m_data.invertedInner[0] = innerCX;
m_data.invertedInner[1] = innerCY;
m_data.invertedInner[2] = innerHW;
m_data.invertedInner[3] = innerHH;
m_bounds = m_bounds.united(QRectF(
static_cast<double>(outerCX) - static_cast<double>(outerHW) - extent,
static_cast<double>(outerCY) - static_cast<double>(outerHH) - extent,
2.0 * (static_cast<double>(outerHW) + extent),
2.0 * (static_cast<double>(outerHH) + extent)));
}
void BlobGroup2::fillMaterial(BlobMaterial* material) {
material->m_paddedX = static_cast<float>(m_bounds.x());
material->m_paddedY = static_cast<float>(m_bounds.y());
material->m_paddedW = static_cast<float>(m_bounds.width());
material->m_paddedH = static_cast<float>(m_bounds.height());
material->m_smoothFactor = static_cast<float>(m_smoothing);
// The single group draw has no owner, so the shader skips the ownership test.
material->m_myIndex = -2;
material->m_color = m_color;
material->m_hasInverted = m_data.hasInverted ? 1 : 0;
material->m_invertedRadius = data().invertedRadius;
memcpy(
material->m_invertedOuter,
data().invertedOuter,
sizeof(data().invertedOuter));
memcpy(
material->m_invertedInner,
data().invertedInner,
sizeof(data().invertedInner));
material->m_shadowRadius = static_cast<float>(m_shadowRadius);
material->m_shadowColor[0] = static_cast<float>(m_shadowColor.redF());
material->m_shadowColor[1] = static_cast<float>(m_shadowColor.greenF());
material->m_shadowColor[2] = static_cast<float>(m_shadowColor.blueF());
material->m_shadowColor[3] = static_cast<float>(m_shadowColor.alphaF());
const int count =
static_cast<int>(qMin(data().rects.size(), qsizetype(16)));
material->m_rectCount = count;
for (int i = 0; i < count; ++i)
material->m_rects[i] = data().rects[i];
}
QSGNode* BlobGroup2::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) {
// Data is built during polish. This only covers a render pass that happens
// without one.
ensureDataBuilt();
if (m_data.rects.isEmpty() && !m_data.hasInverted) {
delete oldNode;
return nullptr;
}
auto* node = static_cast<QSGGeometryNode*>(oldNode);
if (!node) {
node = new QSGGeometryNode;
auto* geometry =
new QSGGeometry(QSGGeometry::defaultAttributes_TexturedPoint2D(), 4);
geometry->setDrawingMode(QSGGeometry::DrawTriangleStrip);
node->setGeometry(geometry);
node->setFlag(QSGNode::OwnsGeometry);
auto* material = new BlobMaterial;
material->setFlag(QSGMaterial::Blending);
node->setMaterial(material);
node->setFlag(QSGNode::OwnsMaterial);
}
// The shader evaluates the field at the scene position a vertex maps to, so
// the group item itself must not be transformed. The vertex positions are the
// bounds in this item's frame; the material carries the same rect in scene
// coordinates.
auto* v = node->geometry()->vertexDataAsTexturedPoint2D();
const QPointF p0 = mapFromScene(m_bounds.topLeft());
const QPointF p1 = mapFromScene(m_bounds.bottomRight());
v[0].set(static_cast<float>(p0.x()), static_cast<float>(p0.y()), 0.0f, 0.0f);
v[1].set(static_cast<float>(p1.x()), static_cast<float>(p0.y()), 1.0f, 0.0f);
v[2].set(static_cast<float>(p0.x()), static_cast<float>(p1.y()), 0.0f, 1.0f);
v[3].set(static_cast<float>(p1.x()), static_cast<float>(p1.y()), 1.0f, 1.0f);
node->markDirty(QSGNode::DirtyGeometry);
fillMaterial(static_cast<BlobMaterial*>(node->material()));
node->markDirty(QSGNode::DirtyMaterial);
return node;
}
+128
View File
@@ -0,0 +1,128 @@
#pragma once
#include "blobmaterial.hpp"
#include <qcolor.h>
#include <qlist.h>
#include <qobject.h>
#include <qqmlengine.h>
#include <qquickitem.h>
#include <qrect.h>
#include <qsgnode.h>
#include <qvector.h>
class BlobShape2;
class BlobInvertedRect2;
// One shared valuation for a whole group, drawn by one node.
//
// The shapes here are data only: they say where the material should be. The group
// draws a single quad over everything the shader can evaluate, so a pixel cannot
// be evaluated by one shape and missed by another, and no second draw can
// disagree with the first. That is what makes overlapping shapes read as one
// surface - one value, one writer, one alpha per pixel.
struct BlobGroupData {
// The shader reads rectData[80] = 16 entries, and excludeMask is a 32-bit
// mask. The shared list is capped at 16, in group order, so the cap is the
// same list for every pixel.
static constexpr int kMaxRects = 16;
QVector<BlobRectData> rects;
bool hasInverted = false;
float invertedRadius = 0;
float invertedOuter[4] = {};
float invertedInner[4] = {};
};
class BlobGroup2 : public QQuickItem {
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(
qreal smoothing READ smoothing WRITE setSmoothing NOTIFY
smoothingChanged)
Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged)
Q_PROPERTY(
bool cornerFill READ cornerFill WRITE setCornerFill NOTIFY
cornerFillChanged)
Q_PROPERTY(
QColor shadowColor READ shadowColor WRITE setShadowColor NOTIFY
shadowColorChanged)
Q_PROPERTY(
qreal shadowRadius READ shadowRadius WRITE setShadowRadius NOTIFY
shadowRadiusChanged)
friend class BlobShape2;
public:
explicit BlobGroup2(QQuickItem* parent = nullptr);
~BlobGroup2() override;
[[nodiscard]] qreal smoothing() const { return m_smoothing; }
void setSmoothing(qreal s);
[[nodiscard]] QColor color() const { return m_color; }
void setColor(const QColor& c);
[[nodiscard]] bool cornerFill() const { return m_cornerFill; }
void setCornerFill(bool e);
[[nodiscard]] QColor shadowColor() const { return m_shadowColor; }
void setShadowColor(const QColor& c);
[[nodiscard]] qreal shadowRadius() const { return m_shadowRadius; }
void setShadowRadius(qreal r);
void addShape(BlobShape2* shape);
void removeShape(BlobShape2* shape);
void setInvertedRect(BlobInvertedRect2* rect);
void clearInvertedRect(BlobInvertedRect2* rect);
[[nodiscard]] const QList<BlobShape2*>& shapes() const { return m_shapes; }
[[nodiscard]] BlobInvertedRect2* invertedRect() const {
return m_invertedRect;
}
[[nodiscard]] const BlobGroupData& data() const { return m_data; }
// One draw per group, so a change to any shape changes the value for every
// pixel. Refresh the whole group.
void markDirty();
signals:
void smoothingChanged();
void colorChanged();
void cornerFillChanged();
void shadowColorChanged();
void shadowRadiusChanged();
protected:
void updatePolish() override;
QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) override;
private:
void ensurePhysicsUpdated();
void ensureDataBuilt();
void fillMaterial(BlobMaterial* material);
qreal m_smoothing = 32.0;
QColor m_color{0x44, 0x88, 0xff};
bool m_cornerFill = true;
QColor m_shadowColor{0, 0, 0, 128};
qreal m_shadowRadius = 0.0;
QList<BlobShape2*> m_shapes;
BlobInvertedRect2* m_invertedRect = nullptr;
bool m_physicsUpdated = false;
bool m_dataBuilt = false;
BlobGroupData m_data;
// Scene-space union of what the shader can evaluate for any shape. The node
// covers exactly this, so gaps are not possible.
QRectF m_bounds;
};
@@ -0,0 +1,44 @@
#include "blobinvertedrect2.hpp"
#include "blobgroup2.hpp"
BlobInvertedRect2::BlobInvertedRect2(QQuickItem* parent) : BlobShape2(parent) {}
BlobInvertedRect2::~BlobInvertedRect2() {
if (m_group) m_group->clearInvertedRect(this);
}
void BlobInvertedRect2::setBorderLeft(qreal v) {
if (qFuzzyCompare(m_borderLeft, v)) return;
m_borderLeft = v;
emit borderLeftChanged();
if (m_group) m_group->markDirty();
}
void BlobInvertedRect2::setBorderRight(qreal v) {
if (qFuzzyCompare(m_borderRight, v)) return;
m_borderRight = v;
emit borderRightChanged();
if (m_group) m_group->markDirty();
}
void BlobInvertedRect2::setBorderTop(qreal v) {
if (qFuzzyCompare(m_borderTop, v)) return;
m_borderTop = v;
emit borderTopChanged();
if (m_group) m_group->markDirty();
}
void BlobInvertedRect2::setBorderBottom(qreal v) {
if (qFuzzyCompare(m_borderBottom, v)) return;
m_borderBottom = v;
emit borderBottomChanged();
if (m_group) m_group->markDirty();
}
void BlobInvertedRect2::registerWithGroup() {
if (m_group) m_group->setInvertedRect(this);
}
void BlobInvertedRect2::unregisterFromGroup() {
if (m_group) m_group->clearInvertedRect(this);
}
@@ -0,0 +1,58 @@
#pragma once
#include "blobshape2.hpp"
#include <qqmlengine.h>
class BlobInvertedRect2 : public BlobShape2 {
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(
qreal borderLeft READ borderLeft WRITE setBorderLeft NOTIFY
borderLeftChanged)
Q_PROPERTY(
qreal borderRight READ borderRight WRITE setBorderRight NOTIFY
borderRightChanged)
Q_PROPERTY(
qreal borderTop READ borderTop WRITE setBorderTop NOTIFY
borderTopChanged)
Q_PROPERTY(
qreal borderBottom READ borderBottom WRITE setBorderBottom NOTIFY
borderBottomChanged)
public:
explicit BlobInvertedRect2(QQuickItem* parent = nullptr);
~BlobInvertedRect2() override;
[[nodiscard]] qreal borderLeft() const { return m_borderLeft; }
void setBorderLeft(qreal v);
[[nodiscard]] qreal borderRight() const { return m_borderRight; }
void setBorderRight(qreal v);
[[nodiscard]] qreal borderTop() const { return m_borderTop; }
void setBorderTop(qreal v);
[[nodiscard]] qreal borderBottom() const { return m_borderBottom; }
void setBorderBottom(qreal v);
signals:
void borderLeftChanged();
void borderRightChanged();
void borderTopChanged();
void borderBottomChanged();
protected:
void registerWithGroup() override;
void unregisterFromGroup() override;
private:
qreal m_borderLeft = 0;
qreal m_borderRight = 0;
qreal m_borderTop = 0;
qreal m_borderBottom = 0;
};
+7 -2
View File
@@ -33,7 +33,7 @@ bool BlobMaterialShader::updateUniformData(
Q_UNUSED(oldMaterial);
auto* mat = static_cast<BlobMaterial*>(newMaterial);
QByteArray* buf = state.uniformData();
Q_ASSERT(buf->size() >= 1440);
Q_ASSERT(buf->size() >= 1472);
if (state.isMatrixDirty()) {
const QMatrix4x4 m = state.combinedMatrix();
@@ -56,7 +56,7 @@ bool BlobMaterialShader::updateUniformData(
// Rect count (offset 88)
memcpy(buf->data() + 88, &mat->m_rectCount, 4);
// My index (offset 92)
// My index (offset 92). -2 skips the ownership test in the shader.
memcpy(buf->data() + 92, &mat->m_myIndex, 4);
// Color as vec4 (offset 96, 16 bytes)
@@ -100,5 +100,10 @@ bool BlobMaterialShader::updateUniformData(
memcpy(buf->data() + base + 64, r.radius, 16);
}
// Shadow: after rectData, which ends at 1440. shadowColor is 16-byte aligned,
// so it lands at 1456.
memcpy(buf->data() + 1440, &mat->m_shadowRadius, 4);
memcpy(buf->data() + 1456, mat->m_shadowColor, 16);
return true;
}
+5
View File
@@ -32,12 +32,17 @@ class BlobMaterial : public QSGMaterial {
float m_paddedH = 0;
float m_smoothFactor = 32.0f;
int m_rectCount = 0;
// -2 means no owner test: the single group draw writes every pixel it covers.
int m_myIndex = -2;
QColor m_color{0x44, 0x88, 0xff};
int m_hasInverted = 0;
float m_invertedRadius = 0;
float m_invertedOuter[4] = {};
float m_invertedInner[4] = {};
// Phase 2: shadow around the material edges. Matches the uniform block, which
// puts these after rectData.
float m_shadowRadius = 0.0f;
float m_shadowColor[4] = {0.0f, 0.0f, 0.0f, 0.5f};
BlobRectData m_rects[16] = {};
};
+73 -46
View File
@@ -5,6 +5,10 @@
#include <cmath>
#include <qnamespace.h>
namespace {
constexpr float k_retireMs = 120.0f;
}
BlobRect::BlobRect(QQuickItem* parent) : BlobShape(parent) {}
BlobRect::~BlobRect() {
@@ -12,41 +16,75 @@ BlobRect::~BlobRect() {
}
void BlobRect::updatePolish() {
if (m_physicsActive) {
float totalDelta = std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) +
std::abs(m_dm11 - 1.0f);
float totalVel =
std::abs(m_dmVel00) + std::abs(m_dmVel01) + std::abs(m_dmVel11);
BlobShape::updatePolish();
if (totalDelta < 0.004f && totalVel < 0.05f) {
if (!m_physicsActive) return;
if (m_retiring) {
const float t = std::clamp(
static_cast<float>(m_retireElapsed.elapsed()) / k_retireMs,
0.0f,
1.0f);
const float s = t * t * (3.0f - 2.0f * t);
if (t >= 1.0f) {
m_dm00 = 1.0f;
m_dm01 = 0.0f;
m_dm11 = 1.0f;
m_dmVel00 = m_dmVel01 = m_dmVel11 = 0.0f;
m_deformMatrix = QMatrix4x4();
emit rawDeformMatrixChanged();
updateCenteredDeformMatrix();
m_retiring = false;
m_physicsActive = false;
if (m_group) {
QMetaObject::invokeMethod(
this,
[this]() {
if (m_group) m_group->markDirty();
},
Qt::QueuedConnection);
}
} else {
QMetaObject::invokeMethod(
this,
[this]() {
if (m_physicsActive && m_group) m_group->markDirty();
},
Qt::QueuedConnection);
m_dm00 = m_retire00 + (1.0f - m_retire00) * s;
m_dm01 = m_retire01 - m_retire01 * s;
m_dm11 = m_retire11 + (1.0f - m_retire11) * s;
m_deformMatrix = QMatrix4x4(
m_dm00,
m_dm01,
0,
0,
m_dm01,
m_dm11,
0,
0,
0,
0,
1,
0,
0,
0,
0,
1);
}
emit rawDeformMatrixChanged();
updateCenteredDeformMatrix();
if (!m_physicsActive) return;
} else {
const float totalDelta = std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) +
std::abs(m_dm11 - 1.0f);
const float totalVel =
std::abs(m_dmVel00) + std::abs(m_dmVel01) + std::abs(m_dmVel11);
if (totalDelta < 0.004f && totalVel < 0.05f) beginRetire();
}
BlobShape::updatePolish();
QMetaObject::invokeMethod(
this,
[this]() {
if (m_physicsActive && m_group) m_group->markDirty();
},
Qt::QueuedConnection);
}
void BlobRect::beginRetire() {
m_retire00 = m_dm00;
m_retire01 = m_dm01;
m_retire11 = m_dm11;
m_dmVel00 = m_dmVel01 = m_dmVel11 = 0.0f;
m_retireElapsed.start();
m_retiring = true;
}
void BlobRect::updatePhysics() {
@@ -74,6 +112,13 @@ void BlobRect::updatePhysics() {
const float speed = std::sqrt(velX * velX + velY * velY);
if (m_retiring) {
if (speed > 5.0f)
m_retiring = false;
else
return;
}
if (!m_physicsActive) {
if (speed < 5.0f) return;
m_physicsActive = true;
@@ -297,6 +342,8 @@ void BlobRect::excludeCornersRemoveLast(QQmlListProperty<BlobRect>* prop) {
}
void BlobRect::checkAtRest(float speed) {
if (m_retiring) return;
constexpr float kEpsilon = 0.002f;
const bool atRest =
std::abs(m_dm00 - 1.0f) < kEpsilon && std::abs(m_dm01) < kEpsilon &&
@@ -304,25 +351,5 @@ void BlobRect::checkAtRest(float speed) {
std::abs(m_dmVel01) < kEpsilon && std::abs(m_dmVel11) < kEpsilon &&
speed < 5.0f;
if (atRest) {
m_dm00 = 1.0f;
m_dm01 = 0.0f;
m_dm11 = 1.0f;
m_dmVel00 = 0.0f;
m_dmVel01 = 0.0f;
m_dmVel11 = 0.0f;
m_deformMatrix = QMatrix4x4();
emit rawDeformMatrixChanged();
updateCenteredDeformMatrix();
m_physicsActive = false;
if (m_group) {
QMetaObject::invokeMethod(
this,
[this]() {
if (m_group) m_group->markDirty();
},
Qt::QueuedConnection);
}
}
if (atRest) beginRetire();
}
+10 -7
View File
@@ -50,9 +50,9 @@ class BlobRect : public BlobShape {
[[nodiscard]] qreal damping() const { return m_damping; }
void setDamping(qreal d) {
if (!qFuzzyCompare(m_damping, d)) {
m_damping = d;
void setDamping(qreal value) {
if (!qFuzzyCompare(m_damping, value)) {
m_damping = value;
emit dampingChanged();
}
}
@@ -108,20 +108,23 @@ class BlobRect : public BlobShape {
private:
void checkAtRest(float speed);
void beginRetire();
// Physics state
QPointF m_prevScenePos;
QElapsedTimer m_elapsed;
bool m_physicsActive = false;
bool m_hasPrevPos = false;
// Symmetric 2x2 deformation matrix components (3 independent: m00, m01,
// m11) Rest state is identity: m00=1, m01=0, m11=1
bool m_retiring = false;
QElapsedTimer m_retireElapsed;
float m_retire00 = 1.0f;
float m_retire01 = 0.0f;
float m_retire11 = 1.0f;
float m_dm00 = 1.0f;
float m_dm01 = 0.0f;
float m_dm11 = 1.0f;
// Spring velocities for each component
float m_dmVel00 = 0.0f;
float m_dmVel01 = 0.0f;
float m_dmVel11 = 0.0f;
+353
View File
@@ -0,0 +1,353 @@
#include "blobrect2.hpp"
#include "blobgroup2.hpp"
#include <algorithm>
#include <cmath>
namespace {
constexpr float k_retireMs = 120.0f;
} // namespace
BlobRect2::BlobRect2(QQuickItem* parent) : BlobShape2(parent) {}
BlobRect2::~BlobRect2() {
if (m_group) m_group->removeShape(this);
}
void BlobRect2::finishAtIdentity() {
m_dm00 = 1.0f;
m_dm01 = 0.0f;
m_dm11 = 1.0f;
m_deformMatrix = QMatrix4x4();
m_retiring = false;
m_physicsActive = false;
emit rawDeformMatrixChanged();
updateCenteredDeformMatrix();
}
void BlobRect2::beginRetire() {
// The settle check would otherwise re-enter this every frame and restart the
// curve timer, so the curve never reaches t = 1.
if (m_retiring) return;
m_retire00 = m_dm00;
m_retire01 = m_dm01;
m_retire11 = m_dm11;
m_dmVel00 = m_dmVel01 = m_dmVel11 = 0.0f;
m_retireElapsed.start();
m_retiring = true;
}
void BlobRect2::updatePhysics() {
// Shapes are data only, so the group drives physics here.
if (m_retiring) {
const float t = std::clamp(
static_cast<float>(m_retireElapsed.elapsed()) / k_retireMs,
0.0f,
1.0f);
const float s = t * t * (3.0f - 2.0f * t);
if (t >= 1.0f) {
finishAtIdentity();
return;
}
m_dm00 = m_retire00 + (1.0f - m_retire00) * s;
m_dm01 = m_retire01 - m_retire01 * s;
m_dm11 = m_retire11 + (1.0f - m_retire11) * s;
m_deformMatrix = QMatrix4x4(
m_dm00, m_dm01, 0, 0, m_dm01, m_dm11, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
emit rawDeformMatrixChanged();
updateCenteredDeformMatrix();
if (!m_physicsActive) return;
}
// No "not active" guard here: an inactive shape still has to be evaluated so it
// can see the panel start moving and become active. The frame loop is only kept
// alive while active, and it is requested before anything can return early —
// requesting it at the end lets a shape stop being evaluated with the matrix
// still off identity, which is what left the panel contents blurry.
if (m_physicsActive && m_group) m_group->markDirty();
const QPointF scenePos = mapToScene(QPointF(width() / 2.0, height() / 2.0));
if (!m_hasPrevPos) {
m_prevScenePos = scenePos;
m_elapsed.start();
m_hasPrevPos = true;
return;
}
const float dt = static_cast<float>(m_elapsed.restart()) / 1000.0f;
if (dt > 0.1f || dt < 0.001f) {
m_prevScenePos = scenePos;
if (m_physicsActive) checkAtRest(0.0f);
return;
}
const float velX =
static_cast<float>(scenePos.x() - m_prevScenePos.x()) / dt;
const float velY =
static_cast<float>(scenePos.y() - m_prevScenePos.y()) / dt;
m_prevScenePos = scenePos;
const float speed = std::sqrt(velX * velX + velY * velY);
if (m_retiring) {
if (speed > 5.0f)
m_retiring = false;
else
return;
}
if (!m_physicsActive) {
if (speed < 5.0f) return;
m_physicsActive = true;
}
const float kStretchFactor = static_cast<float>(m_deformScale);
constexpr float kMaxStretch = 0.35f;
float target00 = 1.0f;
float target01 = 0.0f;
float target11 = 1.0f;
if (speed > 5.0f) {
const float targetStretch =
1.0f + std::min(speed * kStretchFactor, kMaxStretch);
const float targetCompress = 1.0f / targetStretch;
const float cosA = velX / speed;
const float sinA = velY / speed;
const float cos2 = cosA * cosA;
const float sin2 = sinA * sinA;
const float cs = cosA * sinA;
target00 = targetStretch * cos2 + targetCompress * sin2;
target01 = (targetStretch - targetCompress) * cs;
target11 = targetStretch * sin2 + targetCompress * cos2;
}
const float kStiffness = static_cast<float>(m_stiffness);
const float kDamping = static_cast<float>(m_damping);
const float invDamp = 1.0f / (1.0f + kDamping * dt);
m_dmVel00 = (m_dmVel00 - kStiffness * (m_dm00 - target00) * dt) * invDamp;
m_dm00 += m_dmVel00 * dt;
m_dmVel01 = (m_dmVel01 - kStiffness * (m_dm01 - target01) * dt) * invDamp;
m_dm01 += m_dmVel01 * dt;
m_dmVel11 = (m_dmVel11 - kStiffness * (m_dm11 - target11) * dt) * invDamp;
m_dm11 += m_dmVel11 * dt;
m_deformMatrix = QMatrix4x4(
m_dm00, m_dm01, 0, 0, m_dm01, m_dm11, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
emit rawDeformMatrixChanged();
updateCenteredDeformMatrix();
// Start the retire as soon as the matrix is close to identity, as in v1. Waiting
// for every term to fall below kEpsilon keeps the matrix just off identity for
// longer, and the panel content is transformed by it, which reads as blur on
// text.
const float totalDelta =
std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) + std::abs(m_dm11 - 1.0f);
const float totalVel =
std::abs(m_dmVel00) + std::abs(m_dmVel01) + std::abs(m_dmVel11);
// The residual is already inside the rest epsilon, so the eased curve would only
// be aborted or restarted every frame by the panel creeping. Snap instead.
if (totalDelta < 0.004f && totalVel < 0.05f) {
finishAtIdentity();
return;
}
checkAtRest(speed);
}
void BlobRect2::setTopLeftRadius(qreal r) {
if (!qFuzzyCompare(m_topLeftRadius, r)) {
m_topLeftRadius = r;
emit topLeftRadiusChanged();
if (m_group) m_group->markDirty();
}
}
void BlobRect2::setTopRightRadius(qreal r) {
if (!qFuzzyCompare(m_topRightRadius, r)) {
m_topRightRadius = r;
emit topRightRadiusChanged();
if (m_group) m_group->markDirty();
}
}
void BlobRect2::setBottomLeftRadius(qreal r) {
if (!qFuzzyCompare(m_bottomLeftRadius, r)) {
m_bottomLeftRadius = r;
emit bottomLeftRadiusChanged();
if (m_group) m_group->markDirty();
}
}
void BlobRect2::setBottomRightRadius(qreal r) {
if (!qFuzzyCompare(m_bottomRightRadius, r)) {
m_bottomRightRadius = r;
emit bottomRightRadiusChanged();
if (m_group) m_group->markDirty();
}
}
void BlobRect2::cornerRadii(float out[4]) const {
const auto maxR = static_cast<float>(std::min(width(), height())) * 0.5f;
const auto base = std::min(static_cast<float>(m_radius), maxR);
out[0] = std::min(
m_topRightRadius >= 0 ? static_cast<float>(m_topRightRadius) : base,
maxR);
out[1] = std::min(
m_bottomRightRadius >= 0 ? static_cast<float>(m_bottomRightRadius)
: base,
maxR);
out[2] = std::min(
m_bottomLeftRadius >= 0 ? static_cast<float>(m_bottomLeftRadius) : base,
maxR);
out[3] = std::min(
m_topLeftRadius >= 0 ? static_cast<float>(m_topLeftRadius) : base,
maxR);
}
bool BlobRect2::isExcluded(const BlobShape2* other) const {
for (const auto& ptr : m_exclude) {
if (ptr == other) return true;
}
return false;
}
bool BlobRect2::isCornerExcluded(const BlobShape2* other) const {
for (const auto& ptr : m_excludeCorners) {
if (ptr == other) return true;
}
return false;
}
QQmlListProperty<BlobRect2> BlobRect2::exclude() {
return QQmlListProperty<BlobRect2>(
this,
nullptr,
&excludeAppend,
&excludeCount,
&excludeAt,
&excludeClear,
&excludeReplace,
&excludeRemoveLast);
}
QQmlListProperty<BlobRect2> BlobRect2::excludeCorners() {
return QQmlListProperty<BlobRect2>(
this,
nullptr,
&excludeCornersAppend,
&excludeCornersCount,
&excludeCornersAt,
&excludeCornersClear,
&excludeCornersReplace,
&excludeCornersRemoveLast);
}
void BlobRect2::excludeAppend(
QQmlListProperty<BlobRect2>* prop, BlobRect2* rect) {
auto* self = static_cast<BlobRect2*>(prop->object);
self->m_exclude.append(rect);
if (self->m_group) self->m_group->markDirty();
emit self->excludeChanged();
}
qsizetype BlobRect2::excludeCount(QQmlListProperty<BlobRect2>* prop) {
auto* self = static_cast<BlobRect2*>(prop->object);
return self->m_exclude.size();
}
BlobRect2* BlobRect2::excludeAt(
QQmlListProperty<BlobRect2>* prop, qsizetype index) {
auto* self = static_cast<BlobRect2*>(prop->object);
return self->m_exclude.at(index);
}
void BlobRect2::excludeClear(QQmlListProperty<BlobRect2>* prop) {
auto* self = static_cast<BlobRect2*>(prop->object);
if (self->m_exclude.isEmpty()) return;
self->m_exclude.clear();
if (self->m_group) self->m_group->markDirty();
emit self->excludeChanged();
}
void BlobRect2::excludeReplace(
QQmlListProperty<BlobRect2>* prop, qsizetype index, BlobRect2* rect) {
auto* self = static_cast<BlobRect2*>(prop->object);
self->m_exclude[index] = rect;
if (self->m_group) self->m_group->markDirty();
emit self->excludeChanged();
}
void BlobRect2::excludeRemoveLast(QQmlListProperty<BlobRect2>* prop) {
auto* self = static_cast<BlobRect2*>(prop->object);
if (self->m_exclude.isEmpty()) return;
self->m_exclude.removeLast();
if (self->m_group) self->m_group->markDirty();
emit self->excludeChanged();
}
void BlobRect2::excludeCornersAppend(
QQmlListProperty<BlobRect2>* prop, BlobRect2* rect) {
auto* self = static_cast<BlobRect2*>(prop->object);
self->m_excludeCorners.append(rect);
if (self->m_group) self->m_group->markDirty();
emit self->excludeCornersChanged();
}
qsizetype BlobRect2::excludeCornersCount(QQmlListProperty<BlobRect2>* prop) {
auto* self = static_cast<BlobRect2*>(prop->object);
return self->m_excludeCorners.size();
}
BlobRect2* BlobRect2::excludeCornersAt(
QQmlListProperty<BlobRect2>* prop, qsizetype index) {
auto* self = static_cast<BlobRect2*>(prop->object);
return self->m_excludeCorners.at(index);
}
void BlobRect2::excludeCornersClear(QQmlListProperty<BlobRect2>* prop) {
auto* self = static_cast<BlobRect2*>(prop->object);
if (self->m_excludeCorners.isEmpty()) return;
self->m_excludeCorners.clear();
if (self->m_group) self->m_group->markDirty();
emit self->excludeCornersChanged();
}
void BlobRect2::excludeCornersReplace(
QQmlListProperty<BlobRect2>* prop, qsizetype index, BlobRect2* rect) {
auto* self = static_cast<BlobRect2*>(prop->object);
self->m_excludeCorners[index] = rect;
if (self->m_group) self->m_group->markDirty();
emit self->excludeCornersChanged();
}
void BlobRect2::excludeCornersRemoveLast(QQmlListProperty<BlobRect2>* prop) {
auto* self = static_cast<BlobRect2*>(prop->object);
if (self->m_excludeCorners.isEmpty()) return;
self->m_excludeCorners.removeLast();
if (self->m_group) self->m_group->markDirty();
emit self->excludeCornersChanged();
}
void BlobRect2::checkAtRest(float speed) {
if (m_retiring) return;
constexpr float kEpsilon = 0.002f;
const bool atRest =
std::abs(m_dm00 - 1.0f) < kEpsilon && std::abs(m_dm01) < kEpsilon &&
std::abs(m_dm11 - 1.0f) < kEpsilon && std::abs(m_dmVel00) < kEpsilon &&
std::abs(m_dmVel01) < kEpsilon && std::abs(m_dmVel11) < kEpsilon &&
speed < 5.0f;
if (atRest) beginRetire();
}
+191
View File
@@ -0,0 +1,191 @@
#pragma once
#include "blobshape2.hpp"
#include <qelapsedtimer.h>
#include <qpointer.h>
#include <qqmlengine.h>
#include <qqmllist.h>
class BlobRect2 : public BlobShape2 {
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(
qreal stiffness READ stiffness WRITE setStiffness NOTIFY
stiffnessChanged)
Q_PROPERTY(qreal damping READ damping WRITE setDamping NOTIFY dampingChanged)
Q_PROPERTY(
qreal deformScale READ deformScale WRITE setDeformScale NOTIFY
deformScaleChanged)
Q_PROPERTY(
QQmlListProperty<BlobRect2> exclude READ exclude NOTIFY excludeChanged)
Q_PROPERTY(
QQmlListProperty<BlobRect2> excludeCorners READ excludeCorners NOTIFY
excludeCornersChanged)
Q_PROPERTY(
bool atRestOverride READ atRestOverride WRITE setAtRestOverride NOTIFY
atRestOverrideChanged)
Q_PROPERTY(
qreal topLeftRadius READ topLeftRadius WRITE setTopLeftRadius NOTIFY
topLeftRadiusChanged)
Q_PROPERTY(
qreal topRightRadius READ topRightRadius WRITE setTopRightRadius NOTIFY
topRightRadiusChanged)
Q_PROPERTY(
qreal bottomLeftRadius READ bottomLeftRadius WRITE setBottomLeftRadius
NOTIFY bottomLeftRadiusChanged)
Q_PROPERTY(
qreal bottomRightRadius READ bottomRightRadius WRITE
setBottomRightRadius NOTIFY bottomRightRadiusChanged)
public:
explicit BlobRect2(QQuickItem* parent = nullptr);
~BlobRect2() override;
[[nodiscard]] qreal stiffness() const { return m_stiffness; }
void setStiffness(qreal s) {
if (!qFuzzyCompare(m_stiffness, s)) {
m_stiffness = s;
emit stiffnessChanged();
}
}
[[nodiscard]] qreal damping() const { return m_damping; }
void setDamping(qreal value) {
if (!qFuzzyCompare(m_damping, value)) {
m_damping = value;
emit dampingChanged();
}
}
[[nodiscard]] qreal deformScale() const { return m_deformScale; }
void setDeformScale(qreal s) {
if (!qFuzzyCompare(m_deformScale, s)) {
m_deformScale = s;
emit deformScaleChanged();
}
}
QQmlListProperty<BlobRect2> exclude();
QQmlListProperty<BlobRect2> excludeCorners();
[[nodiscard]] bool atRestOverride() const { return m_atRestOverride; }
void setAtRestOverride(bool o) {
if (m_atRestOverride == o) return;
m_atRestOverride = o;
emit atRestOverrideChanged();
if (m_group) m_group->markDirty();
}
bool isExcluded(const BlobShape2* other) const override;
bool isCornerExcluded(const BlobShape2* other) const override;
// The shape that is pushing is the one that can shape the bar edge. A shape that is
// retiring still counts, because the retire curve is still moving the surface.
// atRestOverride keeps the shape in the group's data even at rest; when it is false
// the actual spring state decides, so it is not an override of the state itself.
bool isMoving() const override {
return m_atRestOverride || m_physicsActive || m_retiring;
}
void cornerRadii(float out[4]) const override;
[[nodiscard]] qreal topLeftRadius() const { return m_topLeftRadius; }
void setTopLeftRadius(qreal r);
[[nodiscard]] qreal topRightRadius() const { return m_topRightRadius; }
void setTopRightRadius(qreal r);
[[nodiscard]] qreal bottomLeftRadius() const { return m_bottomLeftRadius; }
void setBottomLeftRadius(qreal r);
[[nodiscard]] qreal bottomRightRadius() const {
return m_bottomRightRadius;
}
void setBottomRightRadius(qreal r);
signals:
void stiffnessChanged();
void dampingChanged();
void deformScaleChanged();
void excludeChanged();
void excludeCornersChanged();
void atRestOverrideChanged();
void topLeftRadiusChanged();
void topRightRadiusChanged();
void bottomLeftRadiusChanged();
void bottomRightRadiusChanged();
protected:
void updatePhysics() override;
private:
void checkAtRest(float speed);
void beginRetire();
void finishAtIdentity();
// Physics state
QPointF m_prevScenePos;
QElapsedTimer m_elapsed;
bool m_physicsActive = false;
bool m_hasPrevPos = false;
// Set from QML. Keeps the shape in the group's data while it is at rest; when it is
// false the spring state decides on its own.
bool m_atRestOverride = false;
bool m_retiring = false;
QElapsedTimer m_retireElapsed;
float m_retire00 = 1.0f;
float m_retire01 = 0.0f;
float m_retire11 = 1.0f;
// Symmetric 2x2 deformation matrix components (3 independent: m00, m01,
// m11) Rest state is identity: m00=1, m01=0, m11=1
float m_dm00 = 1.0f;
float m_dm01 = 0.0f;
float m_dm11 = 1.0f;
// Spring velocities for each component
float m_dmVel00 = 0.0f;
float m_dmVel01 = 0.0f;
float m_dmVel11 = 0.0f;
qreal m_stiffness = 200.0;
qreal m_damping = 16.0;
qreal m_deformScale = 0.0005;
qreal m_topLeftRadius = -1;
qreal m_topRightRadius = -1;
qreal m_bottomLeftRadius = -1;
qreal m_bottomRightRadius = -1;
QList<QPointer<BlobRect2>> m_exclude;
QList<QPointer<BlobRect2>> m_excludeCorners;
static void excludeAppend(
QQmlListProperty<BlobRect2>* prop, BlobRect2* rect);
static qsizetype excludeCount(QQmlListProperty<BlobRect2>* prop);
static BlobRect2* excludeAt(
QQmlListProperty<BlobRect2>* prop, qsizetype index);
static void excludeClear(QQmlListProperty<BlobRect2>* prop);
static void excludeReplace(
QQmlListProperty<BlobRect2>* prop, qsizetype index, BlobRect2* rect);
static void excludeRemoveLast(QQmlListProperty<BlobRect2>* prop);
static void excludeCornersAppend(
QQmlListProperty<BlobRect2>* prop, BlobRect2* rect);
static qsizetype excludeCornersCount(QQmlListProperty<BlobRect2>* prop);
static BlobRect2* excludeCornersAt(
QQmlListProperty<BlobRect2>* prop, qsizetype index);
static void excludeCornersClear(QQmlListProperty<BlobRect2>* prop);
static void excludeCornersReplace(
QQmlListProperty<BlobRect2>* prop, qsizetype index, BlobRect2* rect);
static void excludeCornersRemoveLast(QQmlListProperty<BlobRect2>* prop);
};
+6 -8
View File
@@ -168,8 +168,6 @@ void BlobShape::updatePolish() {
if (other->width() <= 0 || other->height() <= 0) continue;
if (isExcluded(other)) continue;
const QPointF otherScene = other->mapToScene(QPointF(0, 0));
bool include = false;
@@ -194,7 +192,7 @@ void BlobShape::updatePolish() {
const QMatrix4x4& dm = other->m_deformMatrix;
const float a = dm(0, 0), b = dm(1, 0);
const float c = dm(0, 1), d = dm(1, 1);
const float c = dm(0, 1), e = dm(1, 1);
BlobRectData r;
r.cx = static_cast<float>(otherScene.x() + other->width() / 2.0);
@@ -205,19 +203,19 @@ void BlobShape::updatePolish() {
r.offsetX = dm(0, 3);
r.offsetY = dm(1, 3);
const float det = a * d - c * b;
const float det = a * e - c * b;
const float invDet = std::abs(det) > 1e-6f ? 1.0f / det : 1.0f;
r.invDeform[0] = d * invDet;
r.invDeform[0] = e * invDet;
r.invDeform[1] = -b * invDet;
r.invDeform[2] = -c * invDet;
r.invDeform[3] = a * invDet;
const float halfTr = 0.5f * (a + d);
const float halfDiff = 0.5f * (a - d);
const float halfTr = 0.5f * (a + e);
const float halfDiff = 0.5f * (a - e);
r.minEig = halfTr - std::sqrt(halfDiff * halfDiff + c * c);
r.screenHalfX = std::abs(a) * r.hw + std::abs(c) * r.hh;
r.screenHalfY = std::abs(b) * r.hw + std::abs(d) * r.hh;
r.screenHalfY = std::abs(b) * r.hw + std::abs(e) * r.hh;
m_cachedRects.append(r);
rectShapes.append(other);
+67
View File
@@ -0,0 +1,67 @@
#include "blobshape2.hpp"
#include "blobgroup2.hpp"
#include <algorithm>
#include <cmath>
BlobShape2::BlobShape2(QQuickItem* parent) : QQuickItem(parent) {}
void BlobShape2::setGroup(BlobGroup2* g) {
if (m_group == g) return;
if (m_group && isComponentComplete()) unregisterFromGroup();
m_group = g;
if (m_group && isComponentComplete()) registerWithGroup();
emit groupChanged();
if (m_group) m_group->markDirty();
}
void BlobShape2::setRadius(qreal r) {
if (qFuzzyCompare(m_radius, r)) return;
m_radius = r;
emit radiusChanged();
if (m_group) m_group->markDirty();
}
void BlobShape2::componentComplete() {
QQuickItem::componentComplete();
if (m_group) registerWithGroup();
}
void BlobShape2::geometryChange(
const QRectF& newGeometry, const QRectF& oldGeometry) {
QQuickItem::geometryChange(newGeometry, oldGeometry);
updateCenteredDeformMatrix();
// One draw covers the whole group, so any shape change is a group change.
if (m_group) m_group->markDirty();
}
void BlobShape2::updateCenteredDeformMatrix() {
const auto cx = static_cast<float>(width()) * 0.5f;
const auto cy = static_cast<float>(height()) * 0.5f;
QMatrix4x4 result;
result.translate(cx, cy);
result *= m_deformMatrix;
result.translate(-cx, -cy);
if (m_centeredDeformMatrix != result) {
m_centeredDeformMatrix = result;
emit deformMatrixChanged();
}
}
void BlobShape2::cornerRadii(float out[4]) const {
const auto maxR = static_cast<float>(std::min(width(), height())) * 0.5f;
const auto r = std::min(static_cast<float>(m_radius), maxR);
out[0] = r;
out[1] = r;
out[2] = r;
out[3] = r;
}
void BlobShape2::registerWithGroup() {
if (m_group) m_group->addShape(this);
}
void BlobShape2::unregisterFromGroup() {
if (m_group) m_group->removeShape(this);
}
+80
View File
@@ -0,0 +1,80 @@
#pragma once
#include "blobgroup2.hpp"
#include <qmatrix4x4.h>
#include <qquickitem.h>
class BlobGroup2;
// Data-only shape.
//
// The group draws a single node for the whole group, so a shape here contributes
// only what the shader needs: position, radii, exclusion, physics. A shape does
// not create a render node, so no pixel can be evaluated by one shape and missed
// by another.
class BlobShape2 : public QQuickItem {
Q_OBJECT
Q_PROPERTY(BlobGroup2* group READ group WRITE setGroup NOTIFY groupChanged)
Q_PROPERTY(qreal radius READ radius WRITE setRadius NOTIFY radiusChanged)
Q_PROPERTY(
QMatrix4x4 deformMatrix READ deformMatrix NOTIFY deformMatrixChanged)
Q_PROPERTY(
QMatrix4x4 rawDeformMatrix READ rawDeformMatrix NOTIFY
rawDeformMatrixChanged)
friend class BlobGroup2;
public:
explicit BlobShape2(QQuickItem* parent = nullptr);
~BlobShape2() override = default;
[[nodiscard]] BlobGroup2* group() const { return m_group; }
void setGroup(BlobGroup2* g);
[[nodiscard]] qreal radius() const { return m_radius; }
void setRadius(qreal r);
[[nodiscard]] QMatrix4x4 deformMatrix() const {
return m_centeredDeformMatrix;
}
[[nodiscard]] QMatrix4x4 rawDeformMatrix() const { return m_deformMatrix; }
signals:
void groupChanged();
void radiusChanged();
void deformMatrixChanged();
void rawDeformMatrixChanged();
protected:
void componentComplete() override;
void geometryChange(
const QRectF& newGeometry, const QRectF& oldGeometry) override;
virtual bool isExcluded(const BlobShape2*) const { return false; }
// Whether this shape is part of the group's data. Only a pushing shape is, so a shape
// at rest has no part in the draw. BlobRect2 also honours the QML atRestOverride.
virtual bool isMoving() const { return false; }
virtual bool isCornerExcluded(const BlobShape2* /*other*/) const {
return false;
}
virtual void cornerRadii(float out[4]) const;
// Called by the group when it rebuilds the shared data, once per frame.
virtual void updatePhysics() {}
virtual void registerWithGroup();
virtual void unregisterFromGroup();
void updateCenteredDeformMatrix();
BlobGroup2* m_group = nullptr;
qreal m_radius = 0;
QMatrix4x4 m_deformMatrix; // identity by default
QMatrix4x4 m_centeredDeformMatrix;
};
+43 -13
View File
@@ -19,6 +19,8 @@ layout(std140, binding = 0) uniform buf {
vec4 invertedOuter;
vec4 invertedInner;
vec4 rectData[80];
float shadowRadius;
vec4 shadowColor;
};
float sdRoundedBox(vec2 p, vec2 center, vec2 halfSize, float radius) {
@@ -63,9 +65,10 @@ float smaxSharpA(float a, float b, float k) {
void main() {
vec2 pixel = vec2(paddedX, paddedY) + qt_TexCoord0 * vec2(paddedW, paddedH);
// Phase 1: compute per-rect SDF, track owner. We can't smin yet because
// excluded pairs need to skip the smooth blend, which requires pairwise pass
// below.
// Phase 1: compute per-rect SDF, and track which rect owns the pixel for the
// per-shape path (BlobGroup). The single group draw (BlobGroup2) has no owner:
// myIndex stays -2, so the ownership test is skipped and this pass writes every
// pixel the quad covers. Excluded pairs still need the pairwise pass below.
float dArr[16];
int owner = -2;
float minDist = 1e10;
@@ -82,8 +85,10 @@ void main() {
// Offset center for asymmetric deformation
vec2 center = rect.xy + props.yz;
// AABB early-out: skip rects far from this pixel
vec2 extent = sh.xy + vec2(smoothFactor * 1.5);
// AABB early-out: skip rects far from this pixel. The reach has to include the
// shadow, otherwise a rect that is only inside the shadow range would be
// skipped and its distance could not be used.
vec2 extent = sh.xy + vec2(smoothFactor * 1.5 + shadowRadius);
if (abs(pixel.x - center.x) > extent.x ||
abs(pixel.y - center.y) > extent.y) {
dArr[i] = 1e10;
@@ -251,14 +256,39 @@ void main() {
}
}
// Each renderer only outputs pixels it owns, but allow rendering
// blend zones to prevent gaps (mergedSdf < smoothFactor means in blend)
// myIndex == -1: inverted rect renders border-owned pixels
// myIndex >= 0: individual rect renders its owned pixels
if (owner != myIndex && mergedSdf > smoothFactor)
// A pixel this far from every rect cannot have a value, so it is not written.
// The one-unit margin keeps the outermost pixels of the quad out of the AA: their
// derivatives are undefined (the quad neighbour is outside the primitive), which
// is what drew a line along the coverage edge.
float reach = max(smoothFactor, shadowRadius);
if (mergedSdf > reach - 1.0)
discard;
float fw = fwidth(mergedSdf);
float alpha = 1.0 - smoothstep(-fw, fw, mergedSdf);
fragColor = vec4(color.rgb * alpha, alpha) * qt_Opacity;
// Only the owner writes a pixel, which is what makes the per-shape path look
// like one surface: qt_Opacity is applied once per pixel instead of once per
// overlapping draw. The single group draw has no owner (myIndex == -2), so the
// test is skipped and the value below is the only value a pixel can have.
if (myIndex >= -1 && owner != myIndex)
discard;
// Fixed-width AA: fwidth is capped at one unit, so a bad derivative near the quad
// boundary cannot make a wide edge.
float fw = min(fwidth(mergedSdf), 1.0);
float materialAlpha = 1.0 - smoothstep(-fw, fw, mergedSdf);
// Phase 2: the shadow, as a distance falloff from the merged surface. The
// quadratic reaches zero with zero slope at its outer edge, so that edge needs no
// AA of its own. Only the single group draw writes a shadow: in the per-shape
// path overlapping draws would darken the same area twice.
float shadowAlpha = 0.0;
if (shadowRadius > 0.0 && myIndex == -2) {
float t = clamp(mergedSdf / shadowRadius, 0.0, 1.0);
shadowAlpha = (1.0 - t) * (1.0 - t) * shadowColor.a;
}
// Material over shadow, premultiplied.
float totalAlpha = materialAlpha + shadowAlpha * (1.0 - materialAlpha);
vec3 rgb = color.rgb * materialAlpha
+ shadowColor.rgb * shadowAlpha * (1.0 - materialAlpha);
fragColor = vec4(rgb, totalAlpha) * qt_Opacity;
}
+3 -1
View File
@@ -4,14 +4,16 @@ qml_module(ZShell-components
lazylistview.hpp lazylistview.cpp
chatlistview.hpp chatlistview.cpp
wavyline.hpp wavyline.cpp
wavylinerenderer.hpp wavylinerenderer.cpp
buttonrow.hpp buttonrow.cpp
carouselview.hpp carouselview.cpp
wheelinverter.hpp
sparklinecontent.hpp sparklinecontent.cpp
sparklineitem.hpp sparklineitem.cpp
sparklinesceneitem.hpp sparklinesceneitem.cpp
wheelinverter.hpp
LIBRARIES
Qt::Quick
Qt::QuickPrivate
Qt::CanvasPainter
ZShell-internal
)
+11 -11
View File
@@ -187,7 +187,7 @@ void CarouselView::setModel(const QVariantList& m) {
emit modelChanged();
const int newCount = m_model.size();
const int newCount = static_cast<int>(m_model.size());
if (newCount == 0) {
m_currentRealIndex = -1;
@@ -647,7 +647,7 @@ qreal CarouselView::sampleSize(qreal childLoc) const {
}
int CarouselView::realIndexOf(int virtualIndex) const {
const int count = m_model.size();
const int count = static_cast<int>(m_model.size());
if (count == 0) return -1;
@@ -668,7 +668,7 @@ qreal CarouselView::snapTargetX(qreal x) const {
}
void CarouselView::wrapContentIfNeeded() {
const int count = m_model.size();
const int count = static_cast<int>(m_model.size());
if (count == 0) return;
@@ -707,7 +707,7 @@ void CarouselView::wrapContentIfNeeded() {
}
void CarouselView::updateCurrentIndexFromContentX(bool emitPreview) {
const int count = m_model.size();
const int count = static_cast<int>(m_model.size());
if (count == 0) return;
@@ -856,8 +856,8 @@ void CarouselView::positionSlot(const Slot& slot) const {
return rightIndex == n ? m_arrangement.outerSpacing : m_itemSpacing;
};
const auto leftGap = [this, n](int leftIndex) {
return leftIndex == -n ? m_arrangement.outerSpacing : m_itemSpacing;
const auto leftGap = [this, n](int gapIndex) {
return gapIndex == -n ? m_arrangement.outerSpacing : m_itemSpacing;
};
qreal center = 0;
@@ -974,7 +974,7 @@ void CarouselView::releaseAllItems() {
}
void CarouselView::relayout() {
const int count = m_model.size();
const int count = static_cast<int>(m_model.size());
if (count == 0 || width() <= 0 || m_focalWidth <= 0) {
for (Slot& s : m_slots) {
@@ -1038,7 +1038,7 @@ void CarouselView::relayout() {
m_slots.append(Slot{});
slotUsedThisPass.append(false);
freeSlot = m_slots.size() - 1;
freeSlot = static_cast<int>(m_slots.size()) - 1;
}
updateSlotContent(m_slots[freeSlot], v);
@@ -1092,7 +1092,7 @@ void CarouselView::snapToNearest() {
}
void CarouselView::goToIndex(int realIndex) {
const int count = m_model.size();
const int count = static_cast<int>(m_model.size());
if (count == 0) return;
@@ -1126,7 +1126,7 @@ void CarouselView::decrementCurrentIndex() {
}
void CarouselView::centerInstantlyOnReal(int realIndex) {
const int count = m_model.size();
const int count = static_cast<int>(m_model.size());
if (count == 0) return;
@@ -1220,7 +1220,7 @@ qreal CarouselView::sampledVelocity() const {
const VelocitySample& oldest = m_velocitySamples.first();
const VelocitySample& newest = m_velocitySamples.last();
const qreal dt = newest.t - oldest.t;
const qreal dt = static_cast<qreal>(newest.t - oldest.t);
if (dt < 8) return 0;
@@ -252,8 +252,6 @@ int ChatListView::count() const {
}
bool ChatListView::event(QEvent* e) {
// qDebug() << "ChatListView::event() runs";
return QQuickItem::event(e);
}
@@ -366,7 +364,6 @@ void ChatListView::updatePolish() {
m_animateY = false;
const int layoutSize = static_cast<int>(m_layout.size());
const qint64 now = m_clock.elapsed();
bool wrote = false;
for (auto& entry : m_delegates) {
if (!entry.item || entry.pendingRemoval || entry.pendingInsert)
continue;
@@ -397,7 +394,6 @@ void ChatListView::updatePolish() {
entry.item->setY(target);
entry.hasPropY = false;
}
wrote = true;
}
applyAnchor();
@@ -50,8 +50,6 @@ void SparklineContent::setLine2(internal::CircularBuffer* buffer) {
void SparklineContent::connectBuffer(internal::CircularBuffer* buffer) {
if (!buffer) return;
// New data, or the buffer's own smoothingRadius changed (it re-emits
// valuesChanged in that case too) -> geometry is stale.
connect(
buffer,
&internal::CircularBuffer::valuesChanged,
@@ -67,7 +65,7 @@ void SparklineContent::connectBuffer(internal::CircularBuffer* buffer) {
void SparklineContent::setLine1Color(const QColor& color) {
if (m_line1Color == color) return;
m_line1Color = color;
update(); // visual-only, no geometry rebuild needed
update();
}
void SparklineContent::setLine2Color(const QColor& color) {
@@ -173,7 +171,7 @@ SparklineContent::LineGeometry SparklineContent::buildGeometry(
if (m_curveSmoothing && points.size() > 2) {
const qreal tension = 1.0;
const qreal k = tension / 6.0;
const int n = points.size();
const int n = static_cast<int>(points.size());
for (int i = 0; i < n - 1; ++i) {
const QPointF& p0 = points[qMax(i - 1, 0)];
@@ -84,7 +84,6 @@ SparklineSceneItem::LineSample SparklineSceneItem::buildSample(
points.reserve(len);
for (int i = 0; i < len; ++i) {
// Already smoothed once, incrementally, by the buffer itself.
const qreal value =
qBound<qreal>(0.0, buffer->smoothedAt(i), m_maxValue);
@@ -99,7 +98,7 @@ SparklineSceneItem::LineSample SparklineSceneItem::buildSample(
if (m_curveSmoothing && points.size() > 2) {
const qreal tension = 1.0;
const qreal k = tension / 6.0;
const int n = points.size();
const int n = static_cast<int>(points.size());
for (int i = 0; i < n - 1; ++i) {
const QPointF& p0 = points[qMax(i - 1, 0)];
const QPointF& p1 = points[i];
@@ -125,9 +124,6 @@ SparklineSceneItem::LineSample SparklineSceneItem::buildSample(
}
}
// Flatten the curve to a polyline once, here, on data change only.
// This is CPU math only -- no QPainter draw calls, no image, no
// texture upload happen as a result of this.
const auto subpaths = path.toSubpathPolygons();
if (subpaths.isEmpty()) return sample;
@@ -139,7 +135,7 @@ SparklineSceneItem::LineSample SparklineSceneItem::buildSample(
void SparklineSceneItem::writeFillGeometry(
QSGGeometryNode* node, const LineSample& sample) {
const int n = sample.polyline.size();
const int n = static_cast<int>(sample.polyline.size());
auto* geometry = node->geometry();
geometry->allocate(n * 2);
geometry->setDrawingMode(QSGGeometry::DrawTriangleStrip);
@@ -147,8 +143,10 @@ void SparklineSceneItem::writeFillGeometry(
auto* vertices = geometry->vertexDataAsPoint2D();
for (int i = 0; i < n; ++i) {
const QPointF& p = sample.polyline[i];
vertices[i * 2].set(p.x(), p.y());
vertices[i * 2 + 1].set(p.x(), sample.fillBottom);
vertices[i * 2].set(
static_cast<float>(p.x()), static_cast<float>(p.y()));
vertices[i * 2 + 1].set(
static_cast<float>(p.x()), static_cast<float>(sample.fillBottom));
}
node->markDirty(QSGNode::DirtyGeometry);
@@ -156,7 +154,7 @@ void SparklineSceneItem::writeFillGeometry(
void SparklineSceneItem::writeStrokeGeometry(
QSGGeometryNode* node, const LineSample& sample, qreal lineWidth) {
const int n = sample.polyline.size();
const int n = static_cast<int>(sample.polyline.size());
auto* geometry = node->geometry();
geometry->allocate(n * 2);
geometry->setDrawingMode(QSGGeometry::DrawTriangleStrip);
@@ -179,8 +177,12 @@ void SparklineSceneItem::writeStrokeGeometry(
const QPointF& p = sample.polyline[i];
const QPointF offset = normal * half;
vertices[i * 2].set(p.x() + offset.x(), p.y() + offset.y());
vertices[i * 2 + 1].set(p.x() - offset.x(), p.y() - offset.y());
vertices[i * 2].set(
static_cast<float>(p.x() + offset.x()),
static_cast<float>(p.y() + offset.y()));
vertices[i * 2 + 1].set(
static_cast<float>(p.x() - offset.x()),
static_cast<float>(p.y() - offset.y()));
}
node->markDirty(QSGNode::DirtyGeometry);
@@ -243,7 +245,7 @@ QSGNode* SparklineSceneItem::updatePaintNode(
}
QMatrix4x4 matrix;
matrix.translate(-stepX() * m_slideProgress, 0);
matrix.translate(static_cast<float>(-stepX() * m_slideProgress), 0.0f);
transformNode->setMatrix(matrix);
const bool needsGeometryWrite = samplesRebuilt || justCreated;
+9 -96
View File
@@ -1,12 +1,10 @@
#include "wavyline.hpp"
#include <qpainter.h>
#include <qpainterpath.h>
#include "wavylinerenderer.hpp"
namespace ZShell::controls {
WavyLine::WavyLine(QQuickItem* parent)
: QQuickPaintedItem(parent)
: QCanvasPainterItem(parent)
, m_lineWidth(4)
, m_amplitudeMultiplier(0.5)
, m_frequency(6)
@@ -18,10 +16,13 @@ WavyLine::WavyLine(QQuickItem* parent)
, m_startAngle(0)
, m_fullAngle(360)
, m_radius(-1)
, m_value(1)
, m_startAngleRad(0)
, m_fullAngleRad(2 * M_PI) {
setAntialiasing(true);
, m_value(1) {
setFillColor(Qt::transparent);
setAlphaBlending(true);
}
QCanvasPainterItemRenderer* WavyLine::createItemRenderer() const {
return new WavyLineRenderer;
}
int WavyLine::lineWidth() const {
@@ -127,7 +128,6 @@ qreal WavyLine::startAngle() const {
void WavyLine::setStartAngle(qreal startAngle) {
if (!qFuzzyCompare(m_startAngle + 1.0, startAngle + 1.0)) {
m_startAngle = startAngle;
m_startAngleRad = startAngle * M_PI / 180.0;
emit startAngleChanged();
update();
}
@@ -140,7 +140,6 @@ qreal WavyLine::fullAngle() const {
void WavyLine::setFullAngle(qreal fullAngle) {
if (!qFuzzyCompare(m_fullAngle + 1.0, fullAngle + 1.0)) {
m_fullAngle = fullAngle;
m_fullAngleRad = fullAngle * M_PI / 180.0;
emit fullAngleChanged();
update();
}
@@ -170,90 +169,4 @@ void WavyLine::setValue(qreal value) {
}
}
void WavyLine::paint(QPainter* painter) {
painter->setRenderHint(QPainter::Antialiasing);
painter->setPen(QPen(m_color, m_lineWidth, Qt::SolidLine, Qt::RoundCap));
if (m_pathType == Arc) {
paintArc(painter);
} else {
paintLinear(painter);
}
}
void WavyLine::paintLinear(QPainter* painter) {
const auto amplitude = m_lineWidth * m_amplitudeMultiplier;
const auto phase = m_waveProgress * 2 * M_PI;
const auto centerY = height() / 2;
const auto len = m_fullLength > 0 ? m_fullLength : 1;
const auto start = m_lineWidth / 2.0;
const auto fullEnd = width() - m_lineWidth / 2.0;
const auto drawEnd = start + (fullEnd - start) * m_value;
QPainterPath path;
bool first = true;
for (int x = m_lineWidth / 2; x <= drawEnd; ++x) {
const auto theta =
m_frequency * 2 * M_PI * (x + m_startX) / len + phase;
const auto waveY = centerY + amplitude * qSin(theta);
if (first) {
path.moveTo(x, waveY);
first = false;
} else {
path.lineTo(x, waveY);
}
}
painter->drawPath(path);
}
void WavyLine::paintArc(QPainter* painter) {
if (m_fullAngleRad <= 0) {
return;
}
const auto amplitude = m_lineWidth * m_amplitudeMultiplier;
const auto cx = width() / 2.0;
const auto cy = height() / 2.0;
const auto radius =
m_radius > 0
? m_radius
: (qMin(width(), height()) - m_lineWidth - 2 * amplitude) / 2.0;
if (radius <= 0) {
return;
}
const auto phase = m_waveProgress * 2 * M_PI;
const auto arcLen = radius * m_fullAngleRad;
const auto len = m_fullLength > 0 ? m_fullLength : arcLen;
const auto drawAngleRad = m_fullAngleRad * m_value;
if (drawAngleRad <= 0) {
return;
}
const auto N = qMax(64, qCeil(radius * drawAngleRad));
const auto dTheta = drawAngleRad / N;
QPainterPath path;
for (int i = 0; i <= N; ++i) {
const auto theta = m_startAngleRad + i * dTheta;
const auto s = i * dTheta * radius;
const auto phi = m_frequency * 2 * M_PI * (s + m_startX) / len + phase;
const auto r = radius + amplitude * qSin(phi);
const auto px = cx + r * qCos(theta);
const auto py = cy + r * qSin(theta);
if (i == 0) {
path.moveTo(px, py);
} else {
path.lineTo(px, py);
}
}
painter->drawPath(path);
}
} // namespace ZShell::controls
+10 -8
View File
@@ -1,11 +1,14 @@
#pragma once
#include <qqmlintegration.h>
#include <qquickpainteditem.h>
#include <QCanvasPainterItem>
#include <QColor>
namespace ZShell::controls {
class WavyLine : public QQuickPaintedItem {
class WavyLineRenderer;
class WavyLine : public QCanvasPainterItem {
Q_OBJECT
QML_ELEMENT
@@ -82,8 +85,6 @@ class WavyLine : public QQuickPaintedItem {
[[nodiscard]] qreal value() const;
void setValue(qreal value);
void paint(QPainter* painter) override;
signals:
void lineWidthChanged();
void amplitudeMultiplierChanged();
@@ -98,9 +99,12 @@ class WavyLine : public QQuickPaintedItem {
void radiusChanged();
void valueChanged();
protected:
[[nodiscard]] QCanvasPainterItemRenderer* createItemRenderer()
const override;
private:
void paintLinear(QPainter* painter);
void paintArc(QPainter* painter);
friend class WavyLineRenderer;
int m_lineWidth;
qreal m_amplitudeMultiplier;
@@ -114,8 +118,6 @@ class WavyLine : public QQuickPaintedItem {
qreal m_fullAngle;
qreal m_radius;
qreal m_value;
qreal m_startAngleRad;
qreal m_fullAngleRad;
};
} // namespace ZShell::controls
@@ -0,0 +1,116 @@
#include "wavylinerenderer.hpp"
#include "wavyline.hpp"
#include <qcanvaspainter.h>
namespace ZShell::controls {
void WavyLineRenderer::synchronizeData(QCanvasPainterItem* item) {
auto* wavyLine = static_cast<WavyLine*>(item);
m_lineWidth = wavyLine->m_lineWidth;
m_amplitudeMultiplier = wavyLine->m_amplitudeMultiplier;
m_frequency = wavyLine->m_frequency;
m_startX = wavyLine->m_startX;
m_fullLength = wavyLine->m_fullLength;
m_color = wavyLine->m_color;
m_waveProgress = wavyLine->m_waveProgress;
m_pathType = wavyLine->m_pathType;
m_startAngle = wavyLine->m_startAngle;
m_fullAngle = wavyLine->m_fullAngle;
m_radius = wavyLine->m_radius;
m_value = wavyLine->m_value;
}
void WavyLineRenderer::paint(QCanvasPainter* painter) {
painter->clearRect(0, 0, width(), height());
painter->setStrokeStyle(m_color);
painter->setLineWidth(m_lineWidth);
painter->setLineCap(QCanvasPainter::LineCap::Round);
if (m_pathType == WavyLine::Arc) {
paintArc(painter);
} else {
paintLinear(painter);
}
}
void WavyLineRenderer::paintLinear(QCanvasPainter* painter) {
const auto amplitude = m_lineWidth * m_amplitudeMultiplier;
const auto phase = m_waveProgress * 2 * M_PI;
const auto centerY = height() / 2;
const auto len = m_fullLength > 0 ? m_fullLength : 1;
const auto start = m_lineWidth / 2.0;
const auto fullEnd = width() - m_lineWidth / 2.0;
const auto drawEnd = start + (fullEnd - start) * m_value;
painter->beginPath();
bool first = true;
for (int x = m_lineWidth / 2; x <= drawEnd; ++x) {
const auto theta =
m_frequency * 2 * M_PI * (x + m_startX) / len + phase;
const auto waveY = centerY + amplitude * qSin(theta);
if (first) {
painter->moveTo(x, waveY);
first = false;
} else {
painter->lineTo(x, waveY);
}
}
painter->stroke();
}
void WavyLineRenderer::paintArc(QCanvasPainter* painter) {
if (m_fullAngle <= 0) {
return;
}
const auto startAngleRad = m_startAngle * M_PI / 180.0;
const auto fullAngleRad = m_fullAngle * M_PI / 180.0;
const auto amplitude = m_lineWidth * m_amplitudeMultiplier;
const auto cx = width() / 2.0;
const auto cy = height() / 2.0;
const auto radius =
m_radius > 0
? m_radius
: (qMin(width(), height()) - m_lineWidth - 2 * amplitude) / 2.0;
if (radius <= 0) {
return;
}
const auto phase = m_waveProgress * 2 * M_PI;
const auto arcLen = radius * fullAngleRad;
const auto len = m_fullLength > 0 ? m_fullLength : arcLen;
const auto drawAngleRad = fullAngleRad * m_value;
if (drawAngleRad <= 0) {
return;
}
const auto N = qMax(64, qCeil(radius * drawAngleRad));
const auto dTheta = drawAngleRad / N;
painter->beginPath();
for (int i = 0; i <= N; ++i) {
const auto theta = startAngleRad + i * dTheta;
const auto s = i * dTheta * radius;
const auto phi = m_frequency * 2 * M_PI * (s + m_startX) / len + phase;
const auto r = radius + amplitude * qSin(phi);
const auto px = cx + r * qCos(theta);
const auto py = cy + r * qSin(theta);
if (i == 0) {
painter->moveTo(px, py);
} else {
painter->lineTo(px, py);
}
}
painter->stroke();
}
} // namespace ZShell::controls
@@ -0,0 +1,32 @@
#pragma once
#include <QCanvasPainterItemRenderer>
#include <QColor>
#include "wavyline.hpp"
namespace ZShell::controls {
class WavyLineRenderer final : public QCanvasPainterItemRenderer {
public:
void synchronizeData(QCanvasPainterItem* item) override;
void paint(QCanvasPainter* painter) override;
private:
void paintLinear(QCanvasPainter* painter);
void paintArc(QCanvasPainter* painter);
int m_lineWidth;
qreal m_amplitudeMultiplier;
int m_frequency;
qreal m_startX;
qreal m_fullLength;
QColor m_color;
qreal m_waveProgress;
WavyLine::PathType m_pathType;
qreal m_startAngle;
qreal m_fullAngle;
qreal m_radius;
qreal m_value;
};
} // namespace ZShell::controls
+3 -3
View File
@@ -1,11 +1,9 @@
qml_module(ZShell-config
URI ZShell.Config
INCLUDE_PREFIX config
DEPENDENCIES
ZShell.Settings
SOURCES
common.hpp common.cpp
rootnodes.hpp rootnodes.cpp
common.hpp common.cpp
migration.hpp migration.cpp
anim.hpp anim.cpp
fontbuilder.hpp fontbuilder.cpp
@@ -30,6 +28,8 @@ qml_module(ZShell-config
services.hpp
sidebar.hpp
utilities.hpp
DEPENDENCIES
ZShell.Settings
)
target_link_libraries(ZShell-config PUBLIC
+3 -1
View File
@@ -1,9 +1,11 @@
#pragma once
#include "common.hpp"
#include <qhashfunctions.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include "common.hpp"
namespace ZShell::config {
class LlmAppearance : public settings::ObjectNode {
+16
View File
@@ -1,3 +1,8 @@
wl_proto(wlp-layer-shell wlr-layer-shell-unstable-v1 "${CMAKE_CURRENT_SOURCE_DIR}/protocols")
wl_proto(wlp-viewporter viewporter "${WAYLAND_PROTOCOLS}/stable/viewporter")
wl_proto(wlp-fractional-scale fractional-scale-v1 "${WAYLAND_PROTOCOLS}/staging/fractional-scale")
wl_proto(wlp-xdg-shell xdg-shell "${WAYLAND_PROTOCOLS}/stable/xdg-shell")
qml_module(ZShell-internal
URI ZShell.Internal
INCLUDE_PREFIX internal
@@ -9,6 +14,9 @@ qml_module(ZShell-internal
circularbuffer.hpp circularbuffer.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
visualizerbars.hpp visualizerbars.cpp
linearindicatormanager.hpp linearindicatormanager.cpp
@@ -23,4 +31,12 @@ qml_module(ZShell-internal
Qt::Network
Qt::DBus
Qt::CanvasPainter
Qt::GuiPrivate
wlp-layer-shell
wlp-xdg-shell
wlp-viewporter
wlp-fractional-scale
PkgConfig::EGL
PkgConfig::WAYLAND_EGL
PkgConfig::GL
)
+4 -4
View File
@@ -182,10 +182,10 @@ static QString buildHlConfigCall(const QString& key, const QVariant& value) {
}
static QColor colorFromInt(quint32 value) {
const int a = (value >> 24) & 0xFF;
const int r = (value >> 16) & 0xFF;
const int g = (value >> 8) & 0xFF;
const int b = value & 0xFF;
const int a = static_cast<int>(value >> 24) & 0xFF;
const int r = static_cast<int>(value >> 16) & 0xFF;
const int g = static_cast<int>(value >> 8) & 0xFF;
const int b = static_cast<int>(value) & 0xFF;
return QColor(r, g, b, a);
}
@@ -0,0 +1,407 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="wlr_layer_shell_unstable_v1">
<copyright>
Copyright © 2017 Drew DeVault
Permission to use, copy, modify, distribute, and sell this
software and its documentation for any purpose is hereby granted
without fee, provided that the above copyright notice appear in
all copies and that both that copyright notice and this permission
notice appear in supporting documentation, and that the name of
the copyright holders not be used in advertising or publicity
pertaining to distribution of the software without specific,
written prior permission. The copyright holders make no
representations about the suitability of this software for any
purpose. It is provided "as is" without express or implied
warranty.
THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
THIS SOFTWARE.
</copyright>
<interface name="zwlr_layer_shell_v1" version="5">
<description summary="create surfaces that are layers of the desktop">
Clients can use this interface to assign the surface_layer role to
wl_surfaces. Such surfaces are assigned to a "layer" of the output and
rendered with a defined z-depth respective to each other. They may also be
anchored to the edges and corners of a screen and specify input handling
semantics. This interface should be suitable for the implementation of
many desktop shell components, and a broad number of other applications
that interact with the desktop.
</description>
<request name="get_layer_surface">
<description summary="create a layer_surface from a surface">
Create a layer surface for an existing surface. This assigns the role of
layer_surface, or raises a protocol error if another role is already
assigned.
Creating a layer surface from a wl_surface which has a buffer attached
or committed is a client error, and any attempts by a client to attach
or manipulate a buffer prior to the first layer_surface.configure call
must also be treated as errors.
After creating a layer_surface object and setting it up, the client
must perform an initial commit without any buffer attached.
The compositor will reply with a layer_surface.configure event.
The client must acknowledge it and is then allowed to attach a buffer
to map the surface.
You may pass NULL for output to allow the compositor to decide which
output to use. Generally this will be the one that the user most
recently interacted with.
Clients can specify a namespace that defines the purpose of the layer
surface.
</description>
<arg name="id" type="new_id" interface="zwlr_layer_surface_v1"/>
<arg name="surface" type="object" interface="wl_surface"/>
<arg name="output" type="object" interface="wl_output" allow-null="true"/>
<arg name="layer" type="uint" enum="layer" summary="layer to add this surface to"/>
<arg name="namespace_" type="string" summary="namespace for the layer surface"/>
</request>
<enum name="error">
<entry name="role" value="0" summary="wl_surface has another role"/>
<entry name="invalid_layer" value="1" summary="layer value is invalid"/>
<entry name="already_constructed" value="2" summary="wl_surface has a buffer attached or committed"/>
</enum>
<enum name="layer">
<description summary="available layers for surfaces">
These values indicate which layers a surface can be rendered in. They
are ordered by z depth, bottom-most first. Traditional shell surfaces
will typically be rendered between the bottom and top layers.
Fullscreen shell surfaces are typically rendered at the top layer.
Multiple surfaces can share a single layer, and ordering within a
single layer is undefined.
</description>
<entry name="background" value="0"/>
<entry name="bottom" value="1"/>
<entry name="top" value="2"/>
<entry name="overlay" value="3"/>
</enum>
<!-- Version 3 additions -->
<request name="destroy" type="destructor" since="3">
<description summary="destroy the layer_shell object">
This request indicates that the client will not use the layer_shell
object any more. Objects that have been created through this instance
are not affected.
</description>
</request>
</interface>
<interface name="zwlr_layer_surface_v1" version="5">
<description summary="layer metadata interface">
An interface that may be implemented by a wl_surface, for surfaces that
are designed to be rendered as a layer of a stacked desktop-like
environment.
Layer surface state (layer, size, anchor, exclusive zone,
margin, interactivity) is double-buffered, and will be applied at the
time wl_surface.commit of the corresponding wl_surface is called.
Attaching a null buffer to a layer surface unmaps it.
Unmapping a layer_surface means that the surface cannot be shown by the
compositor until it is explicitly mapped again. The layer_surface
returns to the state it had right after layer_shell.get_layer_surface.
The client can re-map the surface by performing a commit without any
buffer attached, waiting for a configure event and handling it as usual.
</description>
<request name="set_size">
<description summary="sets the size of the surface">
Sets the size of the surface in surface-local coordinates. The
compositor will display the surface centered with respect to its
anchors.
If you pass 0 for either value, the compositor will assign it and
inform you of the assignment in the configure event. You must set your
anchor to opposite edges in the dimensions you omit; not doing so is a
protocol error. Both values are 0 by default.
Size is double-buffered, see wl_surface.commit.
</description>
<arg name="width" type="uint"/>
<arg name="height" type="uint"/>
</request>
<request name="set_anchor">
<description summary="configures the anchor point of the surface">
Requests that the compositor anchor the surface to the specified edges
and corners. If two orthogonal edges are specified (e.g. 'top' and
'left'), then the anchor point will be the intersection of the edges
(e.g. the top left corner of the output); otherwise the anchor point
will be centered on that edge, or in the center if none is specified.
Anchor is double-buffered, see wl_surface.commit.
</description>
<arg name="anchor" type="uint" enum="anchor"/>
</request>
<request name="set_exclusive_zone">
<description summary="configures the exclusive geometry of this surface">
Requests that the compositor avoids occluding an area with other
surfaces. The compositor's use of this information is
implementation-dependent - do not assume that this region will not
actually be occluded.
A positive value is only meaningful if the surface is anchored to one
edge or an edge and both perpendicular edges. If the surface is not
anchored, anchored to only two perpendicular edges (a corner), anchored
to only two parallel edges or anchored to all edges, a positive value
will be treated the same as zero.
A positive zone is the distance from the edge in surface-local
coordinates to consider exclusive.
Surfaces that do not wish to have an exclusive zone may instead specify
how they should interact with surfaces that do. If set to zero, the
surface indicates that it would like to be moved to avoid occluding
surfaces with a positive exclusive zone. If set to -1, the surface
indicates that it would not like to be moved to accommodate for other
surfaces, and the compositor should extend it all the way to the edges
it is anchored to.
For example, a panel might set its exclusive zone to 10, so that
maximized shell surfaces are not shown on top of it. A notification
might set its exclusive zone to 0, so that it is moved to avoid
occluding the panel, but shell surfaces are shown underneath it. A
wallpaper or lock screen might set their exclusive zone to -1, so that
they stretch below or over the panel.
The default value is 0.
Exclusive zone is double-buffered, see wl_surface.commit.
</description>
<arg name="zone" type="int"/>
</request>
<request name="set_margin">
<description summary="sets a margin from the anchor point">
Requests that the surface be placed some distance away from the anchor
point on the output, in surface-local coordinates. Setting this value
for edges you are not anchored to has no effect.
The exclusive zone includes the margin.
Margin is double-buffered, see wl_surface.commit.
</description>
<arg name="top" type="int"/>
<arg name="right" type="int"/>
<arg name="bottom" type="int"/>
<arg name="left" type="int"/>
</request>
<enum name="keyboard_interactivity">
<description summary="types of keyboard interaction possible for a layer shell surface">
Types of keyboard interaction possible for layer shell surfaces. The
rationale for this is twofold: (1) some applications are not interested
in keyboard events and not allowing them to be focused can improve the
desktop experience; (2) some applications will want to take exclusive
keyboard focus.
</description>
<entry name="none" value="0">
<description summary="no keyboard focus is possible">
This value indicates that this surface is not interested in keyboard
events and the compositor should never assign it the keyboard focus.
This is the default value, set for newly created layer shell surfaces.
This is useful for e.g. desktop widgets that display information or
only have interaction with non-keyboard input devices.
</description>
</entry>
<entry name="exclusive" value="1">
<description summary="request exclusive keyboard focus">
Request exclusive keyboard focus if this surface is above the shell surface layer.
For the top and overlay layers, the seat will always give
exclusive keyboard focus to the top-most layer which has keyboard
interactivity set to exclusive. If this layer contains multiple
surfaces with keyboard interactivity set to exclusive, the compositor
determines the one receiving keyboard events in an implementation-
defined manner. In this case, no guarantee is made when this surface
will receive keyboard focus (if ever).
For the bottom and background layers, the compositor is allowed to use
normal focus semantics.
This setting is mainly intended for applications that need to ensure
they receive all keyboard events, such as a lock screen or a password
prompt.
</description>
</entry>
<entry name="on_demand" value="2" since="4">
<description summary="request regular keyboard focus semantics">
This requests the compositor to allow this surface to be focused and
unfocused by the user in an implementation-defined manner. The user
should be able to unfocus this surface even regardless of the layer
it is on.
Typically, the compositor will want to use its normal mechanism to
manage keyboard focus between layer shell surfaces with this setting
and regular toplevels on the desktop layer (e.g. click to focus).
Nevertheless, it is possible for a compositor to require a special
interaction to focus or unfocus layer shell surfaces (e.g. requiring
a click even if focus follows the mouse normally, or providing a
keybinding to switch focus between layers).
This setting is mainly intended for desktop shell components (e.g.
panels) that allow keyboard interaction. Using this option can allow
implementing a desktop shell that can be fully usable without the
mouse.
</description>
</entry>
</enum>
<request name="set_keyboard_interactivity">
<description summary="requests keyboard events">
Set how keyboard events are delivered to this surface. By default,
layer shell surfaces do not receive keyboard events; this request can
be used to change this.
This setting is inherited by child surfaces set by the get_popup
request.
Layer surfaces receive pointer, touch, and tablet events normally. If
you do not want to receive them, set the input region on your surface
to an empty region.
Keyboard interactivity is double-buffered, see wl_surface.commit.
</description>
<arg name="keyboard_interactivity" type="uint" enum="keyboard_interactivity"/>
</request>
<request name="get_popup">
<description summary="assign this layer_surface as an xdg_popup parent">
This assigns an xdg_popup's parent to this layer_surface. This popup
should have been created via xdg_surface::get_popup with the parent set
to NULL, and this request must be invoked before committing the popup's
initial state.
See the documentation of xdg_popup for more details about what an
xdg_popup is and how it is used.
</description>
<arg name="popup" type="object" interface="xdg_popup"/>
</request>
<request name="ack_configure">
<description summary="ack a configure event">
When a configure event is received, if a client commits the
surface in response to the configure event, then the client
must make an ack_configure request sometime before the commit
request, passing along the serial of the configure event.
If the client receives multiple configure events before it
can respond to one, it only has to ack the last configure event.
A client is not required to commit immediately after sending
an ack_configure request - it may even ack_configure several times
before its next surface commit.
A client may send multiple ack_configure requests before committing, but
only the last request sent before a commit indicates which configure
event the client really is responding to.
</description>
<arg name="serial" type="uint" summary="the serial from the configure event"/>
</request>
<request name="destroy" type="destructor">
<description summary="destroy the layer_surface">
This request destroys the layer surface.
</description>
</request>
<event name="configure">
<description summary="suggest a surface change">
The configure event asks the client to resize its surface.
Clients should arrange their surface for the new states, and then send
an ack_configure request with the serial sent in this configure event at
some point before committing the new surface.
The client is free to dismiss all but the last configure event it
received.
The width and height arguments specify the size of the window in
surface-local coordinates.
The size is a hint, in the sense that the client is free to ignore it if
it doesn't resize, pick a smaller size (to satisfy aspect ratio or
resize in steps of NxM pixels). If the client picks a smaller size and
is anchored to two opposite anchors (e.g. 'top' and 'bottom'), the
surface will be centered on this axis.
If the width or height arguments are zero, it means the client should
decide its own window dimension.
</description>
<arg name="serial" type="uint"/>
<arg name="width" type="uint"/>
<arg name="height" type="uint"/>
</event>
<event name="closed">
<description summary="surface should be closed">
The closed event is sent by the compositor when the surface will no
longer be shown. The output may have been destroyed or the user may
have asked for it to be removed. Further changes to the surface will be
ignored. The client should destroy the resource after receiving this
event, and create a new surface if they so choose.
</description>
</event>
<enum name="error">
<entry name="invalid_surface_state" value="0" summary="provided surface state is invalid"/>
<entry name="invalid_size" value="1" summary="size is invalid"/>
<entry name="invalid_anchor" value="2" summary="anchor bitfield is invalid"/>
<entry name="invalid_keyboard_interactivity" value="3" summary="keyboard interactivity is invalid"/>
<entry name="invalid_exclusive_edge" value="4" summary="exclusive edge is invalid given the surface anchors"/>
</enum>
<enum name="anchor" bitfield="true">
<entry name="top" value="1" summary="the top edge of the anchor rectangle"/>
<entry name="bottom" value="2" summary="the bottom edge of the anchor rectangle"/>
<entry name="left" value="4" summary="the left edge of the anchor rectangle"/>
<entry name="right" value="8" summary="the right edge of the anchor rectangle"/>
</enum>
<!-- Version 2 additions -->
<request name="set_layer" since="2">
<description summary="change the layer of the surface">
Change the layer that the surface is rendered on.
Layer is double-buffered, see wl_surface.commit.
</description>
<arg name="layer" type="uint" enum="zwlr_layer_shell_v1.layer" summary="layer to move this surface to"/>
</request>
<!-- Version 5 additions -->
<request name="set_exclusive_edge" since="5">
<description summary="set the edge the exclusive zone will be applied to">
Requests an edge for the exclusive zone to apply. The exclusive
edge will be automatically deduced from anchor points when possible,
but when the surface is anchored to a corner, it will be necessary
to set it explicitly to disambiguate, as it is not possible to deduce
which one of the two corner edges should be used.
The edge must be one the surface is anchored to, otherwise the
invalid_exclusive_edge protocol error will be raised.
</description>
<arg name="edge" type="uint" enum="anchor"/>
</request>
</interface>
</protocol>
+1 -1
View File
@@ -72,7 +72,7 @@ static QCanvasPath buildStrokePath(
void StrokeCanvasItem::undoLastGroup() {
if (m_strokes.isEmpty()) return;
const int lastGroup = m_strokes.last().groupId;
for (int i = m_strokes.size() - 1;
for (int i = static_cast<int>(m_strokes.size()) - 1;
i >= 0 && m_strokes[i].groupId == lastGroup;
--i)
m_strokes.remove(i);
@@ -1,6 +1,7 @@
#include "strokecanvasrenderer.hpp"
#include "strokecanvasitem.hpp"
#include <qcolor.h>
#include <QSet>
namespace ZShell::internal {
@@ -148,14 +149,27 @@ void StrokeCanvasRenderer::synchronizeData(QCanvasPainterItem* item) {
m_penColor = canvas->m_penColor;
m_penWidth = canvas->m_penWidth;
while (m_strokes.size() < canvas->m_strokes.size())
m_strokes.append(canvas->m_strokes[m_strokes.size()]);
// Detect removed strokes: present in renderer but no longer in canvas
QSet<int> canvasGroupIds;
for (const auto& stroke : canvas->m_strokes)
canvasGroupIds.insert(stroke.groupId);
if (canvas->m_strokes.isEmpty() && !m_strokes.isEmpty()) {
for (const auto& stroke : m_strokes)
if (stroke.groupId >= 0)
m_pendingGroupRemovals.append(stroke.groupId);
m_strokes.clear();
for (int i = static_cast<int>(m_strokes.size()) - 1; i >= 0; --i) {
if (!canvasGroupIds.contains(m_strokes[i].groupId)) {
if (m_strokes[i].groupId >= 0)
m_pendingGroupRemovals.append(m_strokes[i].groupId);
m_strokes.remove(i);
}
}
// Detect added strokes: present in canvas but not yet in renderer
QSet<int> rendererGroupIds;
for (const auto& stroke : m_strokes)
rendererGroupIds.insert(stroke.groupId);
for (const auto& stroke : canvas->m_strokes) {
if (!rendererGroupIds.contains(stroke.groupId))
m_strokes.append(stroke);
}
m_currentStroke = canvas->m_currentStroke;

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