93 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 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 293bf67e09 better scrollbar in codeblocks + enter anim for delegates 2026-08-27 01:47:17 +02:00
zach c33dbc147f rotated view + wheel inverter 2026-08-26 17:10:34 +02:00
zach 78988700be async parsing + tex caching 2026-08-26 02:06:40 +02:00
zach 3946541f87 async parsing + tex caching 2026-08-26 02:06:10 +02:00
zach e18875a752 add markdown parsing + latex + tree-sitter highlighting for codeblocks in llm responses 2026-08-24 23:55:51 +02:00
zach aae3757daf better chat list view + anchor to bottom. sqlite db for chats + LIFO-ordered 2026-08-23 16:37:11 +02:00
zach 37c6a9986e start initial work on detaching chat to floating window 2026-08-20 20:24:54 +02:00
zach 9657e5092a add removal of chat sessions, auto title + icon, proper stick-to-bottom as response is streamed 2026-08-20 17:40:33 +02:00
zach 47bab21e22 initial commit for llm chat tab in sidebar 2026-08-20 00:27:48 +02:00
222 changed files with 22376 additions and 2153 deletions
+5
View File
@@ -0,0 +1,5 @@
CompileFlags:
Remove:
# GCC-only flags from the system's default spec that clang/clangd cannot
# parse. They appear in compile_commands.json but are not set by the project.
- -mno-direct-extern-access
-1
View File
@@ -38,7 +38,6 @@ body:
attributes: attributes:
label: OS label: OS
options: options:
- NixOS
- Arch - Arch
- Debian / Ubuntu - Debian / Ubuntu
- Fedora - Fedora
+1
View File
@@ -30,6 +30,7 @@ body:
- Refactor / style - Refactor / style
- CI / build / tooling - CI / build / tooling
- Documentation - Documentation
- Other (Describe in description box with header "Issue type")
validations: validations:
required: true required: true
+3
View File
@@ -18,3 +18,6 @@ dist/
network-dev/ network-dev/
**/zshell.build/ **/zshell.build/
**/zshell.dist/ **/zshell.dist/
.opencode/
run-agent.sh
AGENTS.md
+1
View File
@@ -1,3 +1,4 @@
.venv/ .venv/
scripts/fzf.js scripts/fzf.js
scripts/fuzzysort.js scripts/fuzzysort.js
scripts/settings-indexer.js
+2 -1
View File
@@ -3,8 +3,9 @@ FunctionsSpacing=true
IndentWidth=4 IndentWidth=4
MaxColumnWidth=-1 MaxColumnWidth=-1
NewlineType=native NewlineType=native
NormalizeOrder=true GroupAttributesTogether=false
ObjectsSpacing=true ObjectsSpacing=true
SemicolonRule=always SemicolonRule=always
SingleLineEmptyObjects=true
SortImports=false SortImports=false
UseTabs=true UseTabs=true
+31 -22
View File
@@ -26,49 +26,51 @@ if(NOT DEFINED VERSION)
endif() endif()
endif() endif()
if(NOT DEFINED GIT_REVISION)
execute_process(COMMAND git rev-parse HEAD
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
OUTPUT_VARIABLE GIT_REVISION
OUTPUT_STRIP_TRAILING_WHITESPACE
)
if("${GIT_REVISION}" STREQUAL "")
message(FATAL_ERROR "GIT_REVISION is not set and failed to get from git")
endif()
endif()
set(VERSION "${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}") set(VERSION "${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}")
project(ZShell VERSION ${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}) project(ZShell VERSION ${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH})
message(STATUS "ZShell version: ${VERSION}") message(STATUS "ZShell version: ${VERSION}")
include(GNUInstallDirs)
set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib") set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib")
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(ENABLE_MODULES "plugin;shell;m3shapes" CACHE STRING "Modules to build/install") set(ENABLE_MODULES "plugin;shell;cli;m3shapes" CACHE STRING "Modules to build/install")
set(INSTALL_LIBDIR "usr/lib/ZShell" CACHE STRING "Library install dir") set(INSTALL_LIBDIR "${CMAKE_INSTALL_LIBDIR}/ZShell" CACHE STRING "Library install dir")
set(INSTALL_QMLDIR "usr/lib/qt6/qml" CACHE STRING "QML install dir") set(INSTALL_QMLDIR "${CMAKE_INSTALL_LIBDIR}/qt6/qml" CACHE STRING "QML install dir")
set(INSTALL_QSCONFDIR "etc/xdg/quickshell/zshell" CACHE STRING "Quickshell config install dir") set(INSTALL_QSCONFDIR "${CMAKE_INSTALL_SYSCONFDIR}/xdg/quickshell/zshell" CACHE STRING "Quickshell config install dir")
set(INSTALL_GREETERCONFDIR "etc/xdg/quickshell/zshell-greeter" CACHE STRING "Quickshell greeter install dir") set(INSTALL_GREETERCONFDIR "${CMAKE_INSTALL_SYSCONFDIR}/xdg/quickshell/zshell-greeter" CACHE STRING "Quickshell greeter install dir")
set(CMAKE_INSTALL_MESSAGE NEVER) set(CMAKE_INSTALL_MESSAGE NEVER)
add_compile_options( add_compile_options(
-Wall -Wextra -Wpedantic -Wshadow -Wconversion -Wall -Wextra -Wpedantic -Wshadow -Wconversion
-Wold-style-cast -Wnull-dereference -Wdouble-promotion -Wnull-dereference -Wdouble-promotion
-Wformat=2 -Wfloat-equal -Woverloaded-virtual -Wformat=2 -Wfloat-equal
-Wsign-conversion -Wredundant-decls -Wswitch -Wsign-conversion -Wredundant-decls -Wswitch
-Wunreachable-code -Wunreachable-code
$<$<COMPILE_LANGUAGE:CXX>:-Wold-style-cast>
$<$<COMPILE_LANGUAGE:CXX>:-Woverloaded-virtual>
) )
if("shell" IN_LIST ENABLE_MODULES) if("cli" IN_LIST ENABLE_MODULES)
# Build settings index
find_package(Python3 COMPONENTS Interpreter REQUIRED)
set(SETTINGS_INDEX_JSON "${CMAKE_BINARY_DIR}/settings-index.json")
execute_process(
COMMAND ${Python3_EXECUTABLE}
"${CMAKE_SOURCE_DIR}/scripts/build-settings-index.py"
"${CMAKE_SOURCE_DIR}/Modules/Settings"
"${SETTINGS_INDEX_JSON}"
RESULT_VARIABLE SETTINGS_INDEX_RESULT
)
if(NOT SETTINGS_INDEX_RESULT EQUAL 0)
message(FATAL_ERROR "Failed to build settings search index")
endif()
# Nuitka compilation # Nuitka compilation
set(ZSHELL_CLI_BUILD_DIR "${CMAKE_BINARY_DIR}/zshell-cli") set(ZSHELL_CLI_BUILD_DIR "${CMAKE_BINARY_DIR}/zshell-cli")
set(ZSHELL_CLI_DIST "${ZSHELL_CLI_BUILD_DIR}/zshell.dist") set(ZSHELL_CLI_DIST "${ZSHELL_CLI_BUILD_DIR}/zshell.dist")
@@ -188,4 +190,11 @@ if("m3shapes" IN_LIST ENABLE_MODULES)
if(TARGET m3shapesplugin) if(TARGET m3shapesplugin)
set_target_properties(m3shapesplugin PROPERTIES INSTALL_RPATH "$ORIGIN") set_target_properties(m3shapesplugin PROPERTIES INSTALL_RPATH "$ORIGIN")
endif() endif()
# Silence warnings that come from m3shapes files we do not manage.
foreach(m3shapes_target m3shapes_core m3shapes_morph m3shapes_shapes m3shapes m3shapesplugin)
if(TARGET ${m3shapes_target})
target_compile_options(${m3shapes_target} PRIVATE -Wno-shadow -Wno-float-equal)
endif()
endforeach()
endif() endif()
+1 -1
View File
@@ -20,7 +20,7 @@ BusyIndicator {
property int animState property int animState
property color bgColor: Colors.palette.m3secondaryContainer property color bgColor: Colors.palette.m3secondaryContainer
property color fgColor: Colors.palette.m3primary property color fgColor: Colors.palette.m3primary
property real implicitSize: Tokens.font.size.normal * 3 property real implicitSize: Tokens.font.body.medium.pointSize * 3
property real internalStrokeWidth: strokeWidth property real internalStrokeWidth: strokeWidth
readonly property alias progress: manager.progress readonly property alias progress: manager.progress
property real strokeWidth: Tokens.padding.extraSmall * 0.8 property real strokeWidth: Tokens.padding.extraSmall * 0.8
+4 -1
View File
@@ -28,6 +28,9 @@ Flickable {
interval: 10 interval: 10
running: root.doneFakeFlick running: root.doneFakeFlick
onTriggered: root.doneFakeFlick = false onTriggered: {
root.doneFakeFlick = false;
root.returnToBounds();
}
} }
} }
+17 -14
View File
@@ -10,24 +10,27 @@ ListView {
maximumFlickVelocity: 3000 maximumFlickVelocity: 3000
rebound: Transition { rebound: Transition {
onRunningChanged: { // onRunningChanged: {
if (!running && !root.doneFakeFlick) { // if (!running && !root.doneFakeFlick) {
root.doneFakeFlick = true; // root.doneFakeFlick = true;
root.flick(1, 1); // root.flick(1, 1);
root.flick(-1, -1); // root.flick(-1, -1);
Qt.callLater(() => root.cancelFlick()); // Qt.callLater(() => root.cancelFlick());
} // }
} // }
Anim { Anim {
properties: "x,y" properties: "x,y"
type: Anim.DefaultEffects
} }
} }
Timer { // Timer {
interval: 10 // interval: 10
running: root.doneFakeFlick // running: root.doneFakeFlick
//
onTriggered: root.doneFakeFlick = false // onTriggered: {
} // root.doneFakeFlick = false;
// }
// }
} }
+1
View File
@@ -4,6 +4,7 @@ MouseArea {
property int scrollAccumulatedY: 0 property int scrollAccumulatedY: 0
function onWheel(event: WheelEvent): void { function onWheel(event: WheelEvent): void {
event.accepted = false;
} }
onWheel: event => { onWheel: event => {
+1 -2
View File
@@ -6,7 +6,6 @@ Rectangle {
color: "transparent" color: "transparent"
Behavior on color { Behavior on color {
CAnim { CAnim {}
}
} }
} }
+68 -81
View File
@@ -1,33 +1,41 @@
pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Templates import QtQuick.Templates
import qs.Services import qs.Services
import ZShell.Config import ZShell.Config
import qs.Services
ScrollBar { ScrollBar {
id: root id: root
required property Flickable flickable
readonly property bool isHorizontal: flickable && flickable.ScrollBar.horizontal === root
readonly property real axisSize: isHorizontal ? flickable.width : flickable.height
readonly property real axisContentSize: isHorizontal ? flickable.contentWidth : flickable.contentHeight
readonly property real axisContentPos: isHorizontal ? (reversed ? flickable.contentX : (flickable.contentX - flickable.originX)) : (reversed ? flickable.contentY : (flickable.contentY - flickable.originY))
readonly property real axisLength: isHorizontal ? root.width : root.height
readonly property real effectiveSize: Math.max(nonAnimHeight, root.minimumSize) readonly property real effectiveSize: Math.max(nonAnimHeight, root.minimumSize)
readonly property real effectiveTravel: Math.max(0, 1 - root.effectiveSize) readonly property real effectiveTravel: Math.max(0, 1 - root.effectiveSize)
required property Flickable flickable readonly property real nonAnimHeight: root.axisSize / root.axisContentSize
readonly property real nonAnimHeight: flickable.height / flickable.contentHeight readonly property real nonAnimY: root.axisContentPos / root.axisContentSize
readonly property real nonAnimY: flickable.contentY / flickable.contentHeight
readonly property real rawTravel: Math.max(0, 1 - root.nonAnimHeight) readonly property real rawTravel: Math.max(0, 1 - root.nonAnimHeight)
readonly property bool reversed: flickable instanceof ListView && flickable.verticalLayoutDirection === ListView.BottomToTop readonly property bool reversed: isHorizontal ? (flickable instanceof ListView && flickable.layoutDirection === Qt.RightToLeft) : (flickable instanceof ListView && flickable.verticalLayoutDirection === ListView.BottomToTop)
property bool shouldBeActive
readonly property real travelScale: root.rawTravel > 0 ? root.effectiveTravel / root.rawTravel : 0 readonly property real travelScale: root.rawTravel > 0 ? root.effectiveTravel / root.rawTravel : 0
enabled: !ShellState.forActive().isDrawing property bool shouldBeActive
implicitHeight: Tokens.padding.extraSmall * 2 property bool inverted
implicitWidth: Tokens.padding.extraSmall * 2
contentItem: Item { enabled: !ShellState.forActive().isDrawing
}
implicitWidth: size === 1 ? 0 : isHorizontal ? 0 : Tokens.padding.extraSmall * 2
implicitHeight: size === 1 ? 0 : isHorizontal ? Tokens.padding.extraSmall * 2 : 0
contentItem: Item {}
Behavior on position { Behavior on position {
enabled: !fullMouse.pressed enabled: !fullMouse.pressed
Anim { Anim {}
}
} }
onHoveredChanged: { onHoveredChanged: {
@@ -48,54 +56,27 @@ ScrollBar {
target: root.flickable target: root.flickable
} }
CustomClippingRect { Loader {
anchors.bottom: parent.bottom anchors.fill: parent
anchors.right: parent.right active: root.size < 1
anchors.top: parent.top sourceComponent: root.isHorizontal ? horizontalTrack : verticalTrack
implicitWidth: handle.implicitWidth }
radius: Tokens.rounding.full
CustomRect { Component {
id: handle id: verticalTrack
anchors.right: parent.right VerticalScrollBarTrack {
color: Colors.palette.m3secondary scrollBar: root
implicitHeight: root.height * root.effectiveSize mouseArea: fullMouse
implicitWidth: fullMouse.pressed || fullMouse.containsMouse ? Tokens.padding.extraSmall * 2 : Tokens.padding.extraSmall }
opacity: { }
if (!root.enabled)
return 0;
if (root.size === 1)
return 0;
if (fullMouse.pressed)
return 1;
if (fullMouse.containsMouse)
return 0.8;
if (root.policy === ScrollBar.AlwaysOn || root.shouldBeActive)
return 0.6;
return 0;
}
radius: Tokens.rounding.full
y: root.reversed ? root.height * (1 + root.nonAnimY) * root.travelScale : root.height * root.nonAnimY * root.travelScale
Behavior on implicitWidth { Component {
Anim { id: horizontalTrack
}
}
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
MouseArea { HorizontalScrollBarTrack {
id: mouse scrollBar: root
mouseArea: fullMouse
acceptedButtons: Qt.NoButton
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
}
} }
} }
@@ -112,25 +93,40 @@ ScrollBar {
property real pressOffset: 0 property real pressOffset: 0
function contentYFromThumbTop(thumbTop) { function contentPosFromThumbStart(thumbStart) {
var visualPos = root.effectiveTravel > 0 ? thumbTop / root.travelScale : 0; var visualPos = root.effectiveTravel > 0 ? thumbStart / root.travelScale : 0;
return root.reversed ? (visualPos - 1) * root.axisContentSize : visualPos * root.axisContentSize;
return root.reversed ? (visualPos - 1) * root.flickable.contentHeight : visualPos * root.flickable.contentHeight;
} }
function updateFromEvent(event) { function updateFromEvent(event) {
var posInTrack = event.y / root.height; var eventPos = root.isHorizontal ? event.x : event.y;
var thumbTop = posInTrack - pressOffset; var posInTrack = eventPos / root.axisLength;
thumbTop = Math.max(0, Math.min(root.effectiveTravel, thumbTop)); var thumbStart = posInTrack - pressOffset;
thumbStart = Math.max(0, Math.min(root.effectiveTravel, thumbStart));
root.flickable.contentY = contentYFromThumbTop(thumbTop); var newPos = contentPosFromThumbStart(thumbStart);
if (root.isHorizontal)
root.flickable.contentX = newPos + (root.flickable.originX + (root.reversed ? root.flickable.contentWidth : 0));
else
root.flickable.contentY = newPos + (root.flickable.originY + (root.reversed ? root.flickable.contentHeight : 0));
} }
function visualThumbTop() { function visualThumbStart() {
const visualPos = root.reversed ? (1 + root.nonAnimY) : root.nonAnimY; const visualPos = root.reversed ? (1 + root.nonAnimY) : root.nonAnimY;
return visualPos * root.travelScale; return visualPos * root.travelScale;
} }
function onWheel(event: WheelEvent): void {
if (root.horizontal) {
event.accepted = false;
return;
}
var delta = event.angleDelta.y > 0 ? -0.1 : 0.1;
var newPos = Math.max(0, Math.min(1 - root.size, root.position + (root.inverted ? -delta : delta)));
root.position = newPos;
}
anchors.fill: parent anchors.fill: parent
cursorShape: undefined cursorShape: undefined
hoverEnabled: true hoverEnabled: true
@@ -141,24 +137,15 @@ ScrollBar {
updateFromEvent(event); updateFromEvent(event);
} }
onPressed: event => { onPressed: event => {
var currentTop = visualThumbTop(); var currentStart = visualThumbStart();
var currentBottom = currentTop + root.effectiveSize; var currentEnd = currentStart + root.effectiveSize;
var clickPos = event.y / root.height; var eventPos = root.isHorizontal ? event.x : event.y;
var clickPos = eventPos / root.axisLength;
var clickedInsideThumb = clickPos >= currentTop && clickPos <= currentBottom; var clickedInsideThumb = clickPos >= currentStart && clickPos <= currentEnd;
pressOffset = clickedInsideThumb ? (clickPos - currentStart) : root.effectiveSize / 2;
if (clickedInsideThumb) {
pressOffset = clickPos - currentTop;
} else {
pressOffset = root.effectiveSize / 2;
}
updateFromEvent(event); updateFromEvent(event);
} }
onWheel: event => {
var delta = event.angleDelta.y > 0 ? -0.1 : 0.1;
var newPos = Math.max(0, Math.min(1 - root.size, root.position + delta));
root.position = newPos;
}
} }
} }
+5 -3
View File
@@ -16,11 +16,12 @@ Text {
color: Colors.palette.m3onSurface color: Colors.palette.m3onSurface
font: Tokens.font.body.small font: Tokens.font.body.small
linkColor: Colors.palette.m3onPrimaryFixedVariant linkColor: Colors.palette.m3onPrimaryFixedVariant
renderType: Text.NativeRendering renderType: Text.QtRendering
textFormat: Text.PlainText textFormat: Text.PlainText
Behavior on color { Behavior on color {
CAnim {} CAnim {
}
} }
Behavior on text { Behavior on text {
enabled: root.animate enabled: root.animate
@@ -32,7 +33,8 @@ Text {
type: Anim.FastEffects type: Anim.FastEffects
} }
PropertyAction {} PropertyAction {
}
TAnim { TAnim {
target: root target: root
+42
View File
@@ -0,0 +1,42 @@
import QtQuick
import Quickshell
ShaderEffect {
id: root
property Item target: parent
property real glowSize: 40
property real innerGlow: 0
property real lineWidth: 2
property real intensity: 1.0
property real speed: 1.0
property int period: 6000
property bool running: true
property color color1: "#4285F4"
property color color2: "#9B72CB"
property color color3: "#D96570"
property color color4: "#F2A54A"
readonly property size itemSize: Qt.size(width, height)
readonly property size rectSize: target ? Qt.size(target.width, target.height) : Qt.size(0, 0)
readonly property real radius: (target as Rectangle).radius ?? 0
property real phase: 0
anchors.fill: target
anchors.margins: -glowSize
z: -1
enabled: false
fragmentShader: Qt.resolvedUrl(`${Quickshell.shellDir}/assets/shaders/geminiglow.frag.qsb`)
FrameAnimation {
running: root.running && root.visible
onTriggered: {
const dt = frameTime * 1000;
root.phase = (root.phase + dt * root.speed / root.period) % 1.0;
}
}
}
+56
View File
@@ -0,0 +1,56 @@
import QtQuick
import ZShell.Config
import qs.Services
CustomClippingRect {
id: track
required property var scrollBar
required property var mouseArea
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
implicitHeight: handle.implicitHeight
radius: Tokens.rounding.full
CustomRect {
id: handle
anchors.bottom: parent.bottom
color: Colors.palette.m3secondary
implicitWidth: track.scrollBar.width * track.scrollBar.effectiveSize
implicitHeight: track.mouseArea.pressed || track.mouseArea.containsMouse ? Tokens.padding.extraSmall * 2 : Tokens.padding.extraSmall
opacity: {
if (!track.scrollBar.enabled)
return 0;
if (track.scrollBar.size === 1)
return 0;
if (track.mouseArea.pressed)
return 1;
if (track.mouseArea.containsMouse)
return 0.8;
if (track.scrollBar.policy === CustomScrollBar.AlwaysOn || track.scrollBar.shouldBeActive)
return 0.6;
return 0;
}
radius: Tokens.rounding.full
x: track.scrollBar.reversed ? track.scrollBar.width * (1 + track.scrollBar.nonAnimY) * track.scrollBar.travelScale : track.scrollBar.width * track.scrollBar.nonAnimY * track.scrollBar.travelScale
Behavior on implicitHeight {
Anim {}
}
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
MouseArea {
acceptedButtons: Qt.NoButton
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
}
}
}
+1
View File
@@ -9,5 +9,6 @@ CustomText {
property int grade: Colors.light ? 0 : -25 property int grade: Colors.light ? 0 : -25
property font fontStyle: Tokens.font.icon.small 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() 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 model: root.items
CustomRect { CustomRect {
id: item id: menuItem
readonly property bool active: modelData === root?.active readonly property bool active: modelData === root?.active
required property int index required property int index
@@ -142,13 +142,13 @@ MouseArea {
} }
StateLayer { StateLayer {
color: item.active ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurface color: menuItem.active ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurface
enabled: root.expanded enabled: root.expanded
onClicked: { onClicked: {
root.itemSelected(item.modelData); root.itemSelected(menuItem.modelData);
root.active = item.modelData; root.active = menuItem.modelData;
item.modelData.clicked(); menuItem.modelData.clicked();
root.expanded = false; root.expanded = false;
} }
} }
@@ -162,26 +162,27 @@ MouseArea {
MaterialIcon { MaterialIcon {
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
color: item.active ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurfaceVariant color: menuItem.active ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurfaceVariant
text: item.modelData?.icon ?? "" text: menuItem.modelData?.icon ?? ""
visible: text !== ""
} }
CustomText { CustomText {
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true Layout.fillWidth: true
color: item.active ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurface color: menuItem.active ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurface
text: item.modelData?.text ?? "" text: menuItem.modelData?.text ?? ""
} }
Loader { Loader {
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
active: item.modelData?.trailingIcon.length > 0 active: menuItem.modelData?.trailingIcon.length > 0
asynchronous: true asynchronous: true
visible: active visible: active
sourceComponent: MaterialIcon { sourceComponent: MaterialIcon {
color: item.active ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurfaceVariant color: menuItem.active ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurfaceVariant
text: item.modelData.trailingIcon text: menuItem.modelData.trailingIcon
} }
} }
} }
+5 -2
View File
@@ -19,11 +19,11 @@ PersistentProperties {
property bool sidebar property bool sidebar
// Dashboard state // Dashboard state
property int dashboardTab property int dashboardTab: 0
property date dashboardDate: new Date() property date dashboardDate: new Date()
// Sidebar state // Sidebar state
property int sidebarTab property int sidebarTab: 0
property list<string> expandedNotifs: [] property list<string> expandedNotifs: []
// Utilities state // Utilities state
@@ -34,4 +34,7 @@ PersistentProperties {
// Drawing state // Drawing state
property color penColor: "white" property color penColor: "white"
property int penWidth: 4 property int penWidth: 4
// Screenshot state
property bool screenshot
} }
+86
View File
@@ -0,0 +1,86 @@
import QtQuick
import QtQuick.Templates
import ZShell.Config
import qs.Services
TextArea {
id: root
color: Colors.palette.m3onSurface
cursorVisible: !readOnly
font: Tokens.font.body.small
implicitHeight: contentHeight + topPadding + bottomPadding
implicitWidth: contentWidth + leftPadding + rightPadding
placeholderTextColor: Colors.palette.m3onSurfaceVariant // No anim cause placeholder is custom
renderType: TextArea.NativeRendering
selectedTextColor: color
selectionColor: Qt.alpha(Colors.palette.m3primary, 0.4)
verticalAlignment: TextInput.AlignVCenter
Behavior on color {
CAnim {}
}
Behavior on selectionColor {
CAnim {}
}
cursorDelegate: Item {}
CustomRect {
id: cursor
property bool disableBlink
color: Colors.palette.m3primary
implicitHeight: root.cursorRectangle.height
implicitWidth: 1.5
radius: Tokens.rounding.largeIncreased
x: root.cursorRectangle.x
y: root.cursorRectangle.y
Behavior on x {
Anim {
duration: Tokens.anim.durations.expressiveFastEffects
easing.bezierCurve: [0.2, 1, 0.21, 1, 1, 1]
}
}
Behavior on opacity {
Anim {
type: Anim.StandardSmall
}
}
Connections {
function onCursorPositionChanged(): void {
if (root.activeFocus && root.cursorVisible) {
cursor.opacity = 1;
cursor.disableBlink = true;
enableBlink.restart();
}
}
target: root
}
Timer {
id: enableBlink
interval: 500
onTriggered: cursor.disableBlink = false
}
Timer {
interval: 500
repeat: true
running: root.activeFocus && root.cursorVisible && !cursor.disableBlink
triggeredOnStart: true
onTriggered: parent.opacity = parent.opacity === 1 ? 0 : 1
}
Binding {
cursor.opacity: 0
when: !root.activeFocus || !root.cursorVisible
}
}
}
+96
View File
@@ -0,0 +1,96 @@
import QtQuick
import QtQuick.Templates
import ZShell.Config
import qs.Services
TextEdit {
id: root
property bool animateCursor: true
property alias cursor: cursor
color: Colors.palette.m3onSurface
cursorVisible: !readOnly
font: Tokens.font.body.small
renderType: TextField.NativeRendering
selectedTextColor: color
selectionColor: Qt.alpha(Colors.palette.m3primary, 0.4)
verticalAlignment: TextInput.AlignVCenter
Behavior on color {
CAnim {}
}
cursorDelegate: Item {}
Behavior on selectionColor {
CAnim {}
}
CustomRect {
id: cursor
property bool disableBlink
color: Colors.palette.m3primary
implicitHeight: root.cursorRectangle.height
implicitWidth: 1.5
radius: Tokens.rounding.largeIncreased
x: root.cursorRectangle.x
y: root.cursorRectangle.y
Behavior on opacity {
Anim {
type: Anim.StandardSmall
}
}
Behavior on x {
enabled: root.animateCursor
Anim {
duration: Tokens.anim.durations.expressiveFastEffects
easing.bezierCurve: [0.2, 1, 0.21, 1, 1, 1]
}
}
Behavior on y {
enabled: root.animateCursor
Anim {
duration: Tokens.anim.durations.expressiveFastEffects
easing.bezierCurve: [0.2, 1, 0.21, 1, 1, 1]
}
}
Connections {
function onCursorPositionChanged(): void {
if (root.activeFocus && root.cursorVisible) {
cursor.opacity = 1;
cursor.disableBlink = true;
enableBlink.restart();
}
}
target: root
}
Timer {
id: enableBlink
interval: 500
onTriggered: cursor.disableBlink = false
}
Timer {
interval: 500
repeat: true
running: root.activeFocus && root.cursorVisible && !cursor.disableBlink
triggeredOnStart: true
onTriggered: parent.opacity = parent.opacity === 1 ? 0 : 1
}
Binding {
cursor.opacity: 0
when: !root.activeFocus || !root.cursorVisible
}
}
}
+7 -3
View File
@@ -7,7 +7,9 @@ CustomListView {
id: root id: root
property real endFadeOpacity: fadeShouldBeActive(false) ? 0 : 1 property real endFadeOpacity: fadeShouldBeActive(false) ? 0 : 1
property bool fadeEnabled: true
property real fadeAmount: 0.1 property real fadeAmount: 0.1
property real fadeThreshold: 0.0
readonly property bool horizontal: orientation === ListView.Horizontal readonly property bool horizontal: orientation === ListView.Horizontal
property real startFadeOpacity: fadeShouldBeActive(true) ? 0 : 1 property real startFadeOpacity: fadeShouldBeActive(true) ? 0 : 1
@@ -16,7 +18,9 @@ CustomListView {
} }
function fadeShouldBeActive(isStart: bool): bool { function fadeShouldBeActive(isStart: bool): bool {
// When content is smaller than flickable size, hide fade when rebound starts. if (!fadeEnabled)
return false;
if (contentSize() + marginStart() + marginEnd() < viewportSize() && rebound.running && ((isStart ? overshootStart() > 0 : overshootStart() < 0))) { if (contentSize() + marginStart() + marginEnd() < viewportSize() && rebound.running && ((isStart ? overshootStart() > 0 : overshootStart() < 0))) {
return false; return false;
} }
@@ -28,11 +32,11 @@ CustomListView {
} }
function marginEnd(): real { function marginEnd(): real {
return horizontal ? rightMargin : bottomMargin; return horizontal ? rightMargin - fadeThreshold : bottomMargin - fadeThreshold;
} }
function marginStart(): real { function marginStart(): real {
return horizontal ? leftMargin : topMargin; return horizontal ? leftMargin - fadeThreshold : topMargin - fadeThreshold;
} }
function overshootStart(): real { function overshootStart(): real {
+56
View File
@@ -0,0 +1,56 @@
import QtQuick
import ZShell.Config
import qs.Services
CustomClippingRect {
id: track
required property var scrollBar
required property var mouseArea
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.top: parent.top
implicitWidth: handle.implicitWidth
radius: Tokens.rounding.full
CustomRect {
id: handle
anchors.right: parent.right
color: Colors.palette.m3secondary
implicitHeight: track.scrollBar.height * track.scrollBar.effectiveSize
implicitWidth: track.mouseArea.pressed || track.mouseArea.containsMouse ? Tokens.padding.extraSmall * 2 : Tokens.padding.extraSmall
opacity: {
if (!track.scrollBar.enabled)
return 0;
if (track.scrollBar.size === 1)
return 0;
if (track.mouseArea.pressed)
return 1;
if (track.mouseArea.containsMouse)
return 0.8;
if (track.scrollBar.policy === CustomScrollBar.AlwaysOn || track.scrollBar.shouldBeActive)
return 0.6;
return 0;
}
radius: Tokens.rounding.full
y: track.scrollBar.reversed ? track.scrollBar.height * (1 + track.scrollBar.nonAnimY) * track.scrollBar.travelScale : track.scrollBar.height * track.scrollBar.nonAnimY * track.scrollBar.travelScale
Behavior on implicitWidth {
Anim {}
}
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
MouseArea {
acceptedButtons: Qt.NoButton
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
}
}
}
+36 -39
View File
@@ -11,19 +11,18 @@ CustomMouseArea {
required property Item bar required property Item bar
required property real borderThickness required property real borderThickness
required property ScreenState screenState property bool dashboardShortcutActive
property point dragStart
required property Drawing drawing required property Drawing drawing
required property EdgeGeometry geometry required property EdgeGeometry geometry
property bool osdShortcutActive
required property Panels panels required property Panels panels
required property Wrapper popouts required property Wrapper popouts
required property ShellScreen screen required property ShellScreen screen
required property ScreenState screenState
property bool dashboardShortcutActive property bool setInitialPoint: false
property bool osdShortcutActive
property bool singleGestureTriggered: false property bool singleGestureTriggered: false
property bool utilitiesShortcutActive property bool utilitiesShortcutActive
property bool setInitialPoint: false
property point dragStart
function inBottomPanel(panel: Item, x: real, y: real): bool { function inBottomPanel(panel: Item, x: real, y: real): bool {
const panelHeight = panel.height * (1 - (panel.offsetScale ?? 0)); const panelHeight = panel.height * (1 - (panel.offsetScale ?? 0));
@@ -60,39 +59,10 @@ CustomMouseArea {
return x >= panelX && x <= panelX + panel.width; return x >= panelX && x <= panelX + panel.width;
} }
anchors.fill: parent
acceptedButtons: bar.fullscreen ? Qt.NoButton : Qt.AllButtons acceptedButtons: bar.fullscreen ? Qt.NoButton : Qt.AllButtons
anchors.fill: parent
hoverEnabled: true 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: { onContainsMouseChanged: {
if (!containsMouse) { if (!containsMouse) {
if (!osdShortcutActive) { if (!osdShortcutActive) {
@@ -111,7 +81,6 @@ CustomMouseArea {
screenState.dashboard = false; screenState.dashboard = false;
} }
} }
onPositionChanged: event => { onPositionChanged: event => {
const x = event.x; const x = event.x;
const y = event.y; 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) if (pressed && dragStart.y >= screen.height - Config.bar.border && dragStart.x > screen.width / 5 && dragY < -Config.launcher.dragThreshold)
screenState.launcher = true; 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) { if (!dashboardShortcutActive) {
screenState.dashboard = showDashboard; screenState.dashboard = showDashboard;
@@ -159,7 +129,7 @@ CustomMouseArea {
screenState.dashboard = false; 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) { if (dragY > 20) {
screenState.settings = true; screenState.settings = true;
} else if (dragY < -20) { } else if (dragY < -20) {
@@ -212,6 +182,33 @@ CustomMouseArea {
if (geometry.barContains(x, y)) if (geometry.barContains(x, y))
bar.checkPopout(geometry.axisPos(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 { Timer {
id: unhideTimer id: unhideTimer
+1 -1
View File
@@ -151,7 +151,7 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top anchors.top: parent.top
clip: true clip: offsetScale > 0
implicitHeight: dashboard.implicitHeight * (1 - offsetScale) implicitHeight: dashboard.implicitHeight * (1 - offsetScale)
implicitWidth: dashboard.implicitWidth implicitWidth: dashboard.implicitWidth
+46 -51
View File
@@ -52,24 +52,21 @@ CustomWindow {
property var root: Quickshell.shellDir property var root: Quickshell.shellDir
readonly property ScreenState screenState: ShellState.forScreen(screen) readonly property ScreenState screenState: ShellState.forScreen(screen)
readonly property real sdfBorderOffset: 2 * fsTransitionProg readonly property real sdfBorderOffset: 2 * fsTransitionProg
readonly property real shadowOpacity: 0.7 * (1 - (bar.isHovered ? 0 : fsTransitionProg)) readonly property real shadowOpacity: 0.5 * (1 - (bar.isHovered ? 0 : fsTransitionProg))
property color surfaceColor: Colors.tPalette.m3surface property color surfaceColor: Colors.tPalette.m3surface
WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.keyboardFocus: 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 WlrLayershell.layer: (fsTransitionProg > 0 && Config.general.showOverFullscreen) || (hasSpecialWorkspace && hasFullscreenOnNormalWs) ? WlrLayer.Overlay : WlrLayer.Top
color: "transparent" color: "transparent"
contentItem.focus: true mask: screenState.isDrawing || screenState.screenshot ? null : (hasFullscreen && !bar.isHovered ? emptyRegion : regions)
mask: screenState.isDrawing ? null : (hasFullscreen && !bar.isHovered ? emptyRegion : regions)
name: "Bar" name: "Bar"
Behavior on fsTransitionProg { Behavior on fsTransitionProg {
Anim { Anim {}
}
} }
Behavior on surfaceColor { Behavior on surfaceColor {
CAnim { CAnim {}
}
} }
contentItem.Keys.onEscapePressed: { contentItem.Keys.onEscapePressed: {
@@ -145,7 +142,6 @@ CustomWindow {
HyprlandFocusGrab { HyprlandFocusGrab {
id: focusGrab id: focusGrab
windows: [root]
active: { active: {
const s = root.screenState; const s = root.screenState;
const c = Config; const c = Config;
@@ -162,6 +158,7 @@ CustomWindow {
return true; return true;
return false; return false;
} }
windows: [root]
onCleared: { onCleared: {
root.screenState.launcher = false; root.screenState.launcher = false;
@@ -187,23 +184,19 @@ CustomWindow {
id: surface id: surface
anchors.fill: parent anchors.fill: parent
layer.enabled: true
opacity: root.surfaceColor.a opacity: root.surfaceColor.a
layer.effect: MultiEffect { BlobGroup2 {
blurMax: 32
shadowColor: Qt.alpha(Colors.palette.m3shadow, Math.max(0, root.shadowOpacity))
shadowEnabled: true
}
BlobGroup {
id: blobGroup id: blobGroup
anchors.fill: parent
color: root.surfaceColor color: root.surfaceColor
shadowColor: Qt.alpha(Colors.palette.m3shadow, Math.max(0, root.shadowOpacity))
shadowRadius: 18
smoothing: Config.bar.smoothing smoothing: Config.bar.smoothing
} }
BlobInvertedRect { BlobInvertedRect2 {
anchors.fill: parent anchors.fill: parent
anchors.margins: -50 anchors.margins: -50
borderBottom: geometry.insetBottom(root.borderThickness) - anchors.margins - root.sdfBorderOffset borderBottom: geometry.insetBottom(root.borderThickness) - anchors.margins - root.sdfBorderOffset
@@ -253,6 +246,7 @@ CustomWindow {
PanelBg { PanelBg {
id: osdBg id: osdBg
atRestOverride: panels.osd.offsetScale < 0.99
deformAmount: 0.1 deformAmount: 0.1
implicitHeight: panels.osd.height implicitHeight: panels.osd.height
implicitWidth: panels.osd.width implicitWidth: panels.osd.width
@@ -264,6 +258,7 @@ CustomWindow {
PanelBg { PanelBg {
id: notifsBg id: notifsBg
atRestOverride: panel.implicitHeight > 0
panel: panels.notifications panel: panels.notifications
radius: Tokens.rounding.large radius: Tokens.rounding.large
} }
@@ -291,21 +286,23 @@ CustomWindow {
y: panels.popoutsWrapper.y + panels.popouts.y + geometry.insetTop(root.borderThickness) - (geometry.barOnTop ? panels.popouts.height * extraExtent : 0) y: panels.popoutsWrapper.y + panels.popouts.y + geometry.insetTop(root.borderThickness) - (geometry.barOnTop ? panels.popouts.height * extraExtent : 0)
Behavior on extraExtent { Behavior on extraExtent {
Anim { Anim {}
}
} }
} }
PanelBg { PanelBg {
id: resourcesBg id: resourcesBg
property real extraExtent: 0.2
atRestOverride: panels.resources.offsetScale < 0.99
deformAmount: 0.05 deformAmount: 0.05
implicitHeight: panels.resources.height implicitHeight: panels.resources.height * (1 + extraExtent)
implicitWidth: panels.resources.width implicitWidth: panels.resources.width * (1 + extraExtent)
panel: panels.resourcesWrapper panel: panels.resourcesWrapper
radius: Tokens.rounding.largeIncreased + Tokens.padding.small radius: Tokens.rounding.largeIncreased + Tokens.padding.small
x: panels.resourcesWrapper.x + panels.resources.x + geometry.insetLeft(root.borderThickness) x: panels.resourcesWrapper.x + panels.resources.x + geometry.insetLeft(root.borderThickness) - panels.resources.width * extraExtent
y: panels.resourcesWrapper.y + panels.resources.y + geometry.insetTop(root.borderThickness) y: panels.resourcesWrapper.y + panels.resources.y + geometry.insetTop(root.borderThickness) - panels.resources.height * extraExtent
} }
PanelBg { PanelBg {
@@ -335,9 +332,15 @@ CustomWindow {
PanelBg { PanelBg {
id: clipboardBg id: clipboardBg
property real extraExtent: 0.2
deformAmount: 0.03 deformAmount: 0.03
implicitHeight: panels.clipboard.height * (1 + extraExtent)
implicitWidth: panels.clipboard.width * (1 + extraExtent)
panel: panels.clipboard panel: panels.clipboard
radius: 29 radius: 29
x: panels.clipboard.x + geometry.insetLeft(root.borderThickness) - panels.clipboard.width * extraExtent
y: panels.clipboard.y + geometry.insetTop(root.borderThickness)
} }
} }
@@ -345,34 +348,8 @@ CustomWindow {
id: drawing id: drawing
anchors.fill: parent anchors.fill: parent
layer.enabled: true
screenState: root.screenState screenState: root.screenState
z: 2 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 { Interactions2 {
@@ -452,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 property real deformAmount: 0.15
required property Item panel required property Item panel
atRestOverride: panel?.offsetScale < 0.99 ?? false
damping: 28.3
deformScale: (deformAmount * Config.appearance.deform.scale) / 10000 deformScale: (deformAmount * Config.appearance.deform.scale) / 10000
group: blobGroup group: blobGroup
implicitHeight: panel.height implicitHeight: panel.height
-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;
}
}
}
+99 -59
View File
@@ -41,6 +41,7 @@ Item {
property real rsy: Math.min(sy, ey) property real rsy: Math.min(sy, ey)
readonly property real scaleRatio: Hypr.monitorFor(screen).scale readonly property real scaleRatio: Hypr.monitorFor(screen).scale
required property ShellScreen screen required property ShellScreen screen
required property ScreenState screenState
property real sh: Math.abs(sy - ey) property real sh: Math.abs(sy - ey)
readonly property color shadowColor: Hypr.options.decoration.shadow?.color ?? "black" readonly property color shadowColor: Hypr.options.decoration.shadow?.color ?? "black"
readonly property bool shadowEnabled: Hypr.options.decoration.shadow.enabled readonly property bool shadowEnabled: Hypr.options.decoration.shadow.enabled
@@ -97,10 +98,6 @@ Item {
const args = Config.screenshot.mode === "auto" ? ["--rounding", `${rounding}`, "--radius", root.cornerRadius, "--shadow", root.shadowEnabled, "--shadow-blur", `${shadow_blur}`, "--shadow-color", `${r},${g},${b},${a}`, "--shadow-offset-x", root.shadowOffset[0], "--shadow-offset-y", root.shadowOffset[1]] : []; const args = Config.screenshot.mode === "auto" ? ["--rounding", `${rounding}`, "--radius", root.cornerRadius, "--shadow", root.shadowEnabled, "--shadow-blur", `${shadow_blur}`, "--shadow-color", `${r},${g},${b},${a}`, "--shadow-offset-x", root.shadowOffset[0], "--shadow-offset-y", root.shadowOffset[1]] : [];
const postCmd = Config.screenshot.enable_pp ? ["zshell-img-tools", "--scale", root.scaleRatio, ...args, "--image"] : null; const postCmd = Config.screenshot.enable_pp ? ["zshell-img-tools", "--scale", root.scaleRatio, ...args, "--image"] : null;
overlay.visible = border.visible = false;
handles.visible = false;
toolbar.visible = false;
ZShellIo.saveItem(captureLayer, tmpfile, Qt.rect(Math.ceil(rsx), Math.ceil(rsy), Math.floor(sw), Math.floor(sh)), path => { ZShellIo.saveItem(captureLayer, tmpfile, Qt.rect(Math.ceil(rsx), Math.ceil(rsy), Math.floor(sw), Math.floor(sh)), path => {
const finalize = finalPath => { const finalize = finalPath => {
if (copyToClipboard) { if (copyToClipboard) {
@@ -126,39 +123,35 @@ Item {
focus: true focus: true
opacity: 0 opacity: 0
Behavior on rsx {
enabled: !selectArea.pressed && root.mode === "select"
Anim {}
}
Behavior on rsy {
enabled: !selectArea.pressed && root.mode === "select"
Anim {}
}
Behavior on sh {
enabled: !selectArea.pressed && root.mode === "select"
Anim {}
}
Behavior on sw {
enabled: !selectArea.pressed && root.mode === "select"
Anim {}
}
Behavior on opacity { Behavior on opacity {
Anim { Anim {
duration: 300 duration: 300
} }
} }
Behavior on rsx {
enabled: !selectArea.pressed && root.mode === "select"
ExAnim {
}
}
Behavior on rsy {
enabled: !selectArea.pressed && root.mode === "select"
ExAnim {
}
}
Behavior on sh {
enabled: !selectArea.pressed && root.mode === "select"
ExAnim {
}
}
Behavior on sw {
enabled: !selectArea.pressed && root.mode === "select"
ExAnim {
}
}
Component.onCompleted: { Component.onCompleted: {
Hypr.extras.refreshOptions(); Hypr.extras.refreshOptions();
screencopy.active = true; root.forceActiveFocus();
opacity = 1; opacity = 1;
@@ -223,13 +216,63 @@ Item {
onReleased: root.enterEditMode() onReleased: root.enterEditMode()
} }
Connections {
function onClosingChanged(): void {
if (root.loader.closing)
closeAnim.start();
else {
closeAnim.stop();
returnAnim.start();
}
}
target: root.loader
}
ParallelAnimation {
id: returnAnim
Anim {
duration: 300
property: "opacity"
target: root
to: 1
}
Anim {
property: "rsx"
target: root
to: Math.min(root.sx, root.ex)
}
Anim {
property: "rsy"
target: root
to: Math.min(root.sy, root.ey)
}
Anim {
property: "sw"
target: root
to: Math.abs(root.sx - root.ex)
}
Anim {
property: "sh"
target: root
to: Math.abs(root.sy - root.ey)
}
}
SequentialAnimation { SequentialAnimation {
id: closeAnim id: closeAnim
PropertyAction { ScriptAction {
property: "closing" script: {
target: root.loader const states = ShellState.allStates();
value: true for (const s of states)
s.screenshot = false;
}
} }
ParallelAnimation { ParallelAnimation {
@@ -240,19 +283,19 @@ Item {
to: 0 to: 0
} }
ExAnim { Anim {
properties: "rsx,rsy" properties: "rsx,rsy"
target: root target: root
to: 0 to: 0
} }
ExAnim { Anim {
property: "sw" property: "sw"
target: root target: root
to: root.screen.width to: root.screen.width
} }
ExAnim { Anim {
property: "sh" property: "sh"
target: root target: root
to: root.screen.height to: root.screen.height
@@ -271,17 +314,12 @@ Item {
anchors.fill: parent anchors.fill: parent
Loader { ScreencopyView {
id: screencopy id: screencopy
active: false
anchors.fill: parent anchors.fill: parent
asynchronous: true captureSource: root.screen
paintCursor: false
sourceComponent: ScreencopyView {
captureSource: root.screen
paintCursor: false
}
} }
AnnotationCanvas { AnnotationCanvas {
@@ -290,6 +328,7 @@ Item {
anchors.fill: parent anchors.fill: parent
inkColor: root.inkColor inkColor: root.inkColor
layer.enabled: true layer.enabled: true
selection: selectionRect
tool: root.mode === "edit" && root.tool !== "move" ? root.tool : "none" tool: root.mode === "edit" && root.tool !== "move" ? root.tool : "none"
layer.effect: MultiEffect { layer.effect: MultiEffect {
@@ -301,15 +340,14 @@ Item {
} }
} }
Rectangle { CustomRect {
id: overlay id: overlay
anchors.fill: parent anchors.fill: parent
color: "white" color: "white"
layer.enabled: true layer.enabled: true
opacity: 0.3 opacity: 0.3
radius: root.realRounding visible: screencopy.hasContent
visible: screencopy.item?.hasContent ?? false
layer.effect: MultiEffect { layer.effect: MultiEffect {
maskEnabled: true maskEnabled: true
@@ -338,39 +376,44 @@ Item {
} }
} }
Rectangle { CustomRect {
id: border id: border
border.color: Colors.palette.m3primary border.color: Colors.palette.m3primary
border.width: root.realBorderWidth border.width: root.realBorderWidth
color: "transparent"
implicitHeight: selectionRect.implicitHeight + root.realBorderWidth * 2 implicitHeight: selectionRect.implicitHeight + root.realBorderWidth * 2
implicitWidth: selectionRect.implicitWidth + root.realBorderWidth * 2 implicitWidth: selectionRect.implicitWidth + root.realBorderWidth * 2
radius: root.realRounding > 0 ? root.realRounding + root.realBorderWidth : 0 radius: root.realRounding > 0 ? root.realRounding + root.realBorderWidth : 0
visible: screencopy.item?.hasContent ?? false visible: screencopy.hasContent
x: selectionRect.x - root.realBorderWidth x: selectionRect.x - root.realBorderWidth
y: selectionRect.y - root.realBorderWidth y: selectionRect.y - root.realBorderWidth
Behavior on border.color { Behavior on border.color {
Anim { Anim {}
}
} }
} }
SelectionHandles { SelectionHandles {
id: handles id: handles
border: root.realBorderWidth
selH: root.sh selH: root.sh
selW: root.sw selW: root.sw
selX: root.rsx selX: root.rsx
selY: root.rsy selY: root.rsy
selection: selectionRect
tool: root.tool
visible: root.mode === "edit" && root.tool === "move" visible: root.mode === "edit" && root.tool === "move"
onBoundsChanged: (x, y, w, h) => { onBoundsChanged: (x, y, w, h) => {
root.sx = x; let nx = Math.max(0, x);
root.sy = y; let ny = Math.max(0, y);
root.ex = x + w; let nw = Math.min(w, root.screen.width - nx);
root.ey = y + h; let nh = Math.min(h, root.screen.height - ny);
root.sx = nx;
root.sy = ny;
root.ex = nx + nw;
root.ey = ny + nh;
} }
onMoveBy: (dx, dy) => { onMoveBy: (dx, dy) => {
const w = root.sw, h = root.sh; const w = root.sw, h = root.sh;
@@ -401,7 +444,4 @@ Item {
onToolSelected: t => root.tool = t onToolSelected: t => root.tool = t
onUndoRequested: annotations.undo() onUndoRequested: annotations.undo()
} }
component ExAnim: Anim {
}
} }
+51 -56
View File
@@ -8,13 +8,13 @@ Item {
property color inkColor: "#ff3b30" property color inkColor: "#ff3b30"
property real inkWidth: 4 property real inkWidth: 4
required property Rectangle selection
property string tool: "none" property string tool: "none"
signal changed signal changed
function clearAll() { function clearAll() {
committedCanvas.clear(); canvas.clear();
previewCanvas.clear();
changed(); changed();
} }
@@ -54,90 +54,85 @@ Item {
} }
function undo() { function undo() {
committedCanvas.undoLastGroup(); canvas.undoLastGroup();
changed(); changed();
} }
anchors.fill: parent anchors.fill: parent
enabled: tool !== "none"
StrokeCanvas { StrokeCanvas {
id: committedCanvas id: canvas
anchors.fill: parent anchors.fill: parent
penColor: root.inkColor penColor: root.inkColor
penWidth: root.inkWidth penWidth: root.inkWidth
} }
StrokeCanvas { MouseArea {
id: previewCanvas
anchors.fill: parent
penColor: root.inkColor
penWidth: root.inkWidth
}
PointHandler {
id: drawHandler id: drawHandler
property real startX property real startX
property real startY property real startY
property bool started: false
acceptedButtons: Qt.LeftButton | Qt.RightButton acceptedButtons: Qt.LeftButton | Qt.RightButton
enabled: root.tool !== "none" anchors.fill: parent
cursorShape: undefined
enabled: root.enabled
onActiveChanged: { onPositionChanged: e => {
if (active) const x = e.x;
const y = e.y;
if (!pressed)
return; return;
if (root.tool === "pen") { if (root.tool === "pen") {
committedCanvas.endStroke(); canvas.appendPoint(x, y);
} else if (started) {
const pts = root.sampleShape(root.tool, startX, startY, point.position.x, point.position.y);
if (pts.length > 0) {
committedCanvas.beginStroke(pts[0][0], pts[0][1], true);
for (let i = 1; i < pts.length; i++)
committedCanvas.appendPoint(pts[i][0], pts[i][1]);
committedCanvas.endStroke();
}
previewCanvas.clear();
}
started = false;
root.changed();
}
onPointChanged: {
if (!active)
return; return;
}
if (point.pressedButtons & Qt.RightButton) { const pts = root.sampleShape(root.tool, startX, startY, x, y);
root.clearAll(); if (pts.length > 0) {
return; canvas.beginStroke(pts[0][0], pts[0][1], true);
for (let i = 1; i < pts.length; i++)
canvas.appendPoint(pts[i][0], pts[i][1]);
} }
}
onPressed: e => {
if (e.button & Qt.LeftButton) {
startX = e.x;
startY = e.y;
canvas.beginStroke(e.x, e.y);
} else if (e.button & Qt.RightButton) {
root.clearAll();
}
}
onPressedChanged: if (!pressed)
canvas.endStroke()
}
HoverHandler {
cursorShape: {
if (root.tool === "none")
return undefined;
const x = point.position.x; const x = point.position.x;
const y = point.position.y; const y = point.position.y;
const withinXWidth = x > root.selection.x && x < root.selection.x + root.selection.implicitWidth;
const withinYHeight = y > root.selection.y && y < root.selection.y + root.selection.implicitHeight;
if (root.tool === "pen") { if (withinXWidth && withinYHeight) {
if (!started) { if (!drawHandler.pressed)
started = true; canvas.showHover(x, y);
committedCanvas.beginStroke(x, y); else
return; canvas.showHover(drawHandler.mouseX, drawHandler.mouseY);
} return Qt.BlankCursor;
committedCanvas.appendPoint(x, y); } else {
return; canvas.hideHover();
} return undefined;
if (!started) {
started = true;
startX = point.pressPosition.x;
startY = point.pressPosition.y;
}
const pts = root.sampleShape(root.tool, startX, startY, x, y);
if (pts.length > 0) {
previewCanvas.beginStroke(pts[0][0], pts[0][1], true);
for (let i = 1; i < pts.length; i++)
previewCanvas.appendPoint(pts[i][0], pts[i][1]);
} }
} }
enabled: root.enabled
} }
} }
+40 -14
View File
@@ -1,18 +1,23 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import ZShell.Internal
import qs.Services
Item { Item {
id: root id: root
property color handleBorder: "#3584e4" required property int border
property color handleFill: "white" property color handleBorder: Colors.palette.m3primary
property color handleFill: Colors.palette.m3onPrimary
property real hs: 12 property real hs: 12
property real minSize: 24 property real minSize: 24
required property real selH required property real selH
required property real selW required property real selW
required property real selX required property real selX
required property real selY required property real selY
required property Rectangle selection
required property string tool
signal boundsChanged(x: real, y: real, w: real, h: real) signal boundsChanged(x: real, y: real, w: real, h: real)
signal dragFinished signal dragFinished
@@ -23,12 +28,16 @@ Item {
x: selX x: selX
y: selY y: selY
Component.onDestruction: Cursor.clearOverride()
DragHandler { DragHandler {
id: moveHandler id: moveHandler
property real accumX: 0 property real accumX: 0
property real accumY: 0 property real accumY: 0
cursorShape: undefined
dragThreshold: 0
target: null target: null
onActiveChanged: { onActiveChanged: {
@@ -45,51 +54,63 @@ Item {
} }
} }
HoverHandler {
cursorShape: {
if (root.tool !== "move")
return undefined;
if ((moveHandler.centroid.pressedButtons & Qt.LeftButton) && moveHandler.active)
return Qt.ClosedHandCursor;
else
return Qt.OpenHandCursor;
}
}
Handle { Handle {
edge: "tl" edge: "tl"
x: -root.hs / 2 x: -(root.hs + root.border) / 2
y: -root.hs / 2 y: -(root.hs + root.border) / 2
} }
Handle { Handle {
edge: "t" edge: "t"
x: root.width / 2 - root.hs / 2 x: root.width / 2 - root.hs / 2
y: -root.hs / 2 y: -(root.hs + root.border) / 2
} }
Handle { Handle {
edge: "tr" edge: "tr"
x: root.width - root.hs / 2 x: root.width - root.hs / 2 + root.border / 2
y: -root.hs / 2 y: -(root.hs + root.border) / 2
} }
Handle { Handle {
edge: "r" edge: "r"
x: root.width - root.hs / 2 x: root.width - root.hs / 2 + root.border / 2
y: root.height / 2 - root.hs / 2 y: root.height / 2 - root.hs / 2
} }
Handle { Handle {
edge: "br" edge: "br"
x: root.width - root.hs / 2 x: root.width - root.hs / 2 + root.border / 2
y: root.height - root.hs / 2 y: root.height - root.hs / 2 + root.border / 2
} }
Handle { Handle {
edge: "b" edge: "b"
x: root.width / 2 - root.hs / 2 x: root.width / 2 - root.hs / 2
y: root.height - root.hs / 2 y: root.height - root.hs / 2 + root.border / 2
} }
Handle { Handle {
edge: "bl" edge: "bl"
x: -root.hs / 2 x: -(root.hs + root.border) / 2
y: root.height - root.hs / 2 y: root.height - root.hs / 2 + root.border / 2
} }
Handle { Handle {
edge: "l" edge: "l"
x: -root.hs / 2 x: -(root.hs + root.border) / 2
y: root.height / 2 - root.hs / 2 y: root.height / 2 - root.hs / 2
} }
@@ -107,6 +128,8 @@ Item {
z: 10 z: 10
HoverHandler { HoverHandler {
id: hover
cursorShape: { cursorShape: {
switch (handle.edge) { switch (handle.edge) {
case "tl": case "tl":
@@ -132,6 +155,7 @@ Item {
property real startX property real startX
property real startY property real startY
dragThreshold: 0
grabPermissions: PointerHandler.CanTakeOverFromAnything | PointerHandler.TakeOverForbidden grabPermissions: PointerHandler.CanTakeOverFromAnything | PointerHandler.TakeOverForbidden
target: null target: null
@@ -141,8 +165,10 @@ Item {
startY = root.y; startY = root.y;
startW = root.width; startW = root.width;
startH = root.height; startH = root.height;
Cursor.setOverride(hover.cursorShape);
} else { } else {
root.dragFinished(); root.dragFinished();
Cursor.clearOverride();
} }
} }
onTranslationChanged: { onTranslationChanged: {
+38
View File
@@ -5,7 +5,9 @@ import Quickshell
import Quickshell.Io import Quickshell.Io
import QtQuick import QtQuick
import ZShell.Models import ZShell.Models
import ZShell.Internal
import ZShell.Config import ZShell.Config
import qs.Components
import qs.Services import qs.Services
import qs.Helpers import qs.Helpers
import qs.Paths import qs.Paths
@@ -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 Wrapper popouts
required property ClipWrapper popoutsWrapper required property ClipWrapper popoutsWrapper
required property ShellScreen screen required property ShellScreen screen
readonly property int vPadding: 6
required property ScreenState screenState required property ScreenState screenState
readonly property int vPadding: 6
function axisCenterOf(item: Item): real { function axisCenterOf(item: Item): real {
const c = item.mapToItem(root, item.implicitWidth / 2, item.implicitHeight / 2); const c = item.mapToItem(root, item.implicitWidth / 2, item.implicitHeight / 2);
return horizontal ? c.x : c.y; return horizontal ? Math.floor(c.x) : Math.floor(c.y);
} }
function checkPopout(pos: real): void { function checkPopout(pos: real): void {
@@ -210,7 +210,8 @@ GridLayout {
roleValue: "network" roleValue: "network"
delegate: EntryWrapper { delegate: EntryWrapper {
NetworkWidget {} NetworkWidget {
}
} }
} }
+4 -8
View File
@@ -63,8 +63,7 @@ WidgetBase {
visible: root.horizontal visible: root.horizontal
Behavior on color { Behavior on color {
CAnim { CAnim {}
}
} }
} }
@@ -88,8 +87,7 @@ WidgetBase {
text: Qt.formatDateTime(Time.date, modelData) text: Qt.formatDateTime(Time.date, modelData)
Behavior on color { Behavior on color {
CAnim { CAnim {}
}
} }
} }
} }
@@ -126,8 +124,7 @@ WidgetBase {
} }
Behavior on color { Behavior on color {
CAnim { CAnim {}
}
} }
} }
} }
@@ -140,8 +137,7 @@ WidgetBase {
visible: Config.services.useTwelveHourClock && root.formatParts.timeTokens.length > 0 visible: Config.services.useTwelveHourClock && root.formatParts.timeTokens.length > 0
Behavior on color { Behavior on color {
CAnim { CAnim {}
}
} }
} }
} }
+31 -17
View File
@@ -1,6 +1,6 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Shapes import Quickshell
import qs.Components import qs.Components
import ZShell.Config import ZShell.Config
import qs.Services import qs.Services
@@ -12,6 +12,7 @@ GridLayout {
property real animatedPercentage: 0 property real animatedPercentage: 0
readonly property real arcStartAngle: 0.75 * Math.PI readonly property real arcStartAngle: 0.75 * Math.PI
readonly property real arcSweep: 1.5 * Math.PI readonly property real arcSweep: 1.5 * Math.PI
readonly property real dpr: (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1
required property bool horizontal required property bool horizontal
property string icon property string icon
required property color iconColor required property color iconColor
@@ -22,12 +23,17 @@ GridLayout {
property bool warning: percentage * 100 >= warningThreshold property bool warning: percentage * 100 >= warningThreshold
property int warningThreshold: 80 property int warningThreshold: 80
function snap(v) {
return Math.round(v * dpr) / dpr;
}
columnSpacing: Tokens.spacing.medium columnSpacing: Tokens.spacing.medium
columns: horizontal ? -1 : 1 columns: horizontal ? -1 : 1
percentage: 0 percentage: 0
Behavior on animatedPercentage { Behavior on animatedPercentage {
Anim {} Anim {
}
} }
Component.onCompleted: animatedPercentage = percentage Component.onCompleted: animatedPercentage = percentage
@@ -47,27 +53,35 @@ GridLayout {
text: root.icon text: root.icon
} }
CustomClippingRect { CustomRect {
id: track
Layout.preferredHeight: root.horizontal ? icon.implicitHeight - Tokens.padding.extraSmall : 4 Layout.preferredHeight: root.horizontal ? icon.implicitHeight - Tokens.padding.extraSmall : 4
Layout.preferredWidth: root.horizontal ? 4 : icon.implicitWidth Layout.preferredWidth: root.horizontal ? 4 : icon.implicitWidth
color: Colors.layer(Colors.palette.m3surfaceContainerHigh, 2) color: Colors.layer(Colors.palette.m3surfaceContainerHigh, 2)
radius: Tokens.rounding.full radius: Math.min(width, height) / 2
CustomRect { Item {
id: fill id: clipper
anchors.bottom: parent.bottom anchors.bottom: root.horizontal ? parent.bottom : undefined
anchors.left: parent.left anchors.horizontalCenter: root.horizontal ? parent.horizontalCenter : undefined
anchors.right: root.horizontal ? parent.right : undefined anchors.left: root.horizontal ? undefined : parent.left
anchors.top: root.horizontal ? undefined : parent.top anchors.verticalCenter: root.horizontal ? undefined : parent.verticalCenter
color: root.mainColor clip: true
implicitHeight: root.horizontal ? Math.ceil(root.percentage * parent.height) : 0 height: root.horizontal ? root.snap(root.percentage * track.height) : track.height * 2
implicitWidth: root.horizontal ? 0 : Math.ceil(root.percentage * parent.width) width: root.horizontal ? track.width * 2 : root.snap(root.percentage * track.width)
// Behavior on implicitHeight { CustomRect {
// Anim { anchors.bottom: root.horizontal ? parent.bottom : undefined
// } anchors.horizontalCenter: root.horizontal ? parent.horizontalCenter : undefined
// } anchors.left: root.horizontal ? undefined : parent.left
anchors.verticalCenter: root.horizontal ? undefined : parent.verticalCenter
color: root.mainColor
height: track.height
radius: track.radius
width: track.width
}
} }
} }
} }
+4 -2
View File
@@ -36,8 +36,8 @@ CustomClippingRect {
MaterialIcon { MaterialIcon {
id: noUpdatesIcon id: noUpdatesIcon
anchors.horizontalCenter: parent.horizontalCenter anchors.centerIn: parent
anchors.top: parent.top anchors.verticalCenterOffset: -noUpdatesText.implicitHeight
color: Colors.tPalette.m3onSurfaceVariant color: Colors.tPalette.m3onSurfaceVariant
fontStyle: Tokens.font.icon.builders.large.size(48).weight(Font.DemiBold).build() fontStyle: Tokens.font.icon.builders.large.size(48).weight(Font.DemiBold).build()
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
@@ -45,6 +45,8 @@ CustomClippingRect {
} }
CustomText { CustomText {
id: noUpdatesText
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.top: noUpdatesIcon.bottom anchors.top: noUpdatesIcon.bottom
color: Colors.tPalette.m3onSurfaceVariant color: Colors.tPalette.m3onSurfaceVariant
+41 -65
View File
@@ -1,7 +1,6 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import Quickshell import Quickshell
import Quickshell.Widgets
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Config import ZShell.Config
@@ -11,11 +10,6 @@ import qs.Components
Item { Item {
id: root 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: { readonly property var dashboardTabs: {
const allTabs = [ const allTabs = [
{ {
@@ -45,9 +39,12 @@ Item {
]; ];
return allTabs.filter(tab => tab.enabled); 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 implicitHeight: nonAnimHeight
implicitWidth: nonAnimWidth
Behavior on implicitHeight { Behavior on implicitHeight {
Anim { Anim {
@@ -63,15 +60,14 @@ Item {
Tabs { Tabs {
id: tabs id: tabs
anchors.top: parent.top
anchors.left: parent.left anchors.left: parent.left
anchors.margins: Tokens.padding.small
anchors.right: parent.right anchors.right: parent.right
anchors.top: parent.top
anchors.topMargin: ZUtils.clamp(anchors.margins - Config.bar.border, 0, anchors.margins)
nonAnimWidth: view.implicitWidth nonAnimWidth: view.implicitWidth
screenState: root.screenState screenState: root.screenState
tabs: root.dashboardTabs tabs: root.dashboardTabs
anchors.topMargin: ZUtils.clamp(anchors.margins - Config.bar.border, 0, anchors.margins)
anchors.margins: Tokens.padding.small
} }
Item { Item {
@@ -79,15 +75,15 @@ Item {
property int radius: Tokens.rounding.extraLarge - anchors.margins 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.bottom: parent.bottom
anchors.topMargin: Tokens.spacing.small anchors.left: parent.left
anchors.margins: Tokens.padding.small 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 id: view
readonly property int currentIndex: root.screenState.dashboardTab readonly property int currentIndex: root.screenState.dashboardTab
@@ -97,64 +93,50 @@ Item {
} }
anchors.fill: parent anchors.fill: parent
implicitWidth: currentItem?.implicitWidth ?? 0
implicitHeight: currentItem?.implicitHeight ?? 0 implicitHeight: currentItem?.implicitHeight ?? 0
implicitWidth: currentItem?.implicitWidth ?? 0
contentX: currentItem?.x ?? 0 Repeater {
contentWidth: row.implicitWidth id: repeater
contentHeight: row.implicitHeight
interactive: false delegate: Loader {
id: paneLoader
onContentXChanged: { required property int index
if (!moving || !currentItem) required property var modelData
return;
const x = contentX - currentItem.x; readonly property real targetX: index < view.currentIndex ? -view.implicitWidth : index > view.currentIndex ? view.implicitWidth : 0
if (x > currentItem.implicitWidth / 2) readonly property bool settled: x === targetX
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);
}
RowLayout { active: index === view.currentIndex || opacity > 0
id: row opacity: index === view.currentIndex ? 1 : 0
sourceComponent: modelData.component
visible: opacity > 0
x: targetX
y: 0
Repeater { Behavior on opacity {
id: repeater Anim {
type: Anim.DefaultEffects
model: ScriptModel { }
values: root.dashboardTabs
} }
Behavior on x {
delegate: Loader { Anim {
id: paneLoader type: Anim.DefaultEffects
}
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);
})
} }
} }
model: ScriptModel {
values: root.dashboardTabs
}
} }
Component { Component {
id: dashComponent id: dashComponent
Dash { Dash {
screenState: root.screenState
radius: viewWrapper.radius radius: viewWrapper.radius
screenState: root.screenState
} }
} }
@@ -170,8 +152,8 @@ Item {
id: mediaComponent id: mediaComponent
Media { Media {
screenState: root.screenState
radius: viewWrapper.radius radius: viewWrapper.radius
screenState: root.screenState
} }
} }
@@ -182,12 +164,6 @@ Item {
radius: viewWrapper.radius radius: viewWrapper.radius
} }
} }
Behavior on contentX {
Anim {
type: Anim.DefaultEffects
}
}
} }
} }
} }
+18 -1
View File
@@ -25,6 +25,8 @@ CustomListView {
return [0]; return [0];
case "variant": case "variant":
return SchemeVariants.query(text); return SchemeVariants.query(text);
case "files":
return Files.query(text);
default: default:
return Apps.search(text); return Apps.search(text);
} }
@@ -33,7 +35,7 @@ CustomListView {
function stateForText(text: string): string { function stateForText(text: string): string {
const prefix = Config.launcher.actionPrefix; const prefix = Config.launcher.actionPrefix;
if (text.startsWith(prefix)) { if (text.startsWith(prefix)) {
for (const action of ["calc", "scheme", "variant"]) for (const action of ["calc", "scheme", "variant", "files"])
if (text.startsWith(`${prefix}${action} `)) if (text.startsWith(`${prefix}${action} `))
return action; return action;
@@ -163,6 +165,13 @@ CustomListView {
PropertyChanges { PropertyChanges {
root.delegate: variantItem root.delegate: variantItem
} }
},
State {
name: "files"
PropertyChanges {
root.delegate: filesItem
}
} }
] ]
transitions: Transition { transitions: Transition {
@@ -264,6 +273,14 @@ CustomListView {
} }
} }
Component {
id: filesItem
FilesItem {
list: root
}
}
Connections { Connections {
function onTextChanged() { function onTextChanged() {
root.syncDisplayText(); root.syncDisplayText();
+2
View File
@@ -85,6 +85,8 @@ Item {
} else if (text.startsWith(Config.launcher.actionPrefix)) { } else if (text.startsWith(Config.launcher.actionPrefix)) {
if (text.startsWith(`${Config.launcher.actionPrefix}calc `)) if (text.startsWith(`${Config.launcher.actionPrefix}calc `))
currentItem.onClicked(); currentItem.onClicked();
else if (text.startsWith(`${Config.launcher.actionPrefix}files `))
currentItem.onClicked();
else else
currentItem.modelData.onClicked(list.currentList); currentItem.modelData.onClicked(list.currentList);
} else { } else {
+1 -1
View File
@@ -16,7 +16,7 @@ Item {
implicitHeight: Config.launcher.sizes.itemHeight implicitHeight: Config.launcher.sizes.itemHeight
StateLayer { StateLayer {
radius: Tokens.rounding.small radius: Tokens.rounding.medium
onClicked: { onClicked: {
root.modelData?.onClicked(root.list); root.modelData?.onClicked(root.list);
+1 -1
View File
@@ -19,7 +19,7 @@ Item {
implicitHeight: Config.launcher.sizes.itemHeight implicitHeight: Config.launcher.sizes.itemHeight
StateLayer { StateLayer {
radius: Tokens.rounding.small radius: Tokens.rounding.medium
onClicked: { onClicked: {
Apps.launch(root.modelData); Apps.launch(root.modelData);
+2 -3
View File
@@ -24,7 +24,7 @@ Item {
implicitHeight: Config.launcher.sizes.itemHeight implicitHeight: Config.launcher.sizes.itemHeight
StateLayer { StateLayer {
radius: Tokens.rounding.small radius: Tokens.rounding.medium
onClicked: { onClicked: {
root.onClicked(); root.onClicked();
@@ -97,8 +97,7 @@ Item {
text: qsTr("Open in calculator") text: qsTr("Open in calculator")
Behavior on opacity { Behavior on opacity {
Anim { Anim {}
}
} }
} }
+1 -1
View File
@@ -15,7 +15,7 @@ Item {
implicitHeight: Config.launcher.sizes.itemHeight implicitHeight: Config.launcher.sizes.itemHeight
StateLayer { StateLayer {
radius: Tokens.rounding.small radius: Tokens.rounding.medium
onClicked: { onClicked: {
root.modelData?.onClicked(root.list); root.modelData?.onClicked(root.list);
+194
View File
@@ -0,0 +1,194 @@
// qs/Services/Files.qml
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import ZShell.Config
Singleton {
id: root
property list<var> results: []
property string _pendingQuery: ""
property int _generation: 0
readonly property var _imageExts: ({
"png": true,
"jpg": true,
"jpeg": true,
"gif": true,
"bmp": true,
"webp": true,
"svg": true,
"tiff": true,
"tif": true,
"ico": true,
"heic": true,
"avif": true
})
// freedesktop mimetype-icon names; extend as needed
readonly property var _extIcons: ({
// images
"png": "image-x-generic",
"jpg": "image-x-generic",
"jpeg": "image-x-generic",
"gif": "image-x-generic",
"bmp": "image-x-generic",
"webp": "image-x-generic",
"svg": "image-x-generic",
"tiff": "image-x-generic",
"tif": "image-x-generic",
"ico": "image-x-generic",
"heic": "image-x-generic",
"avif": "image-x-generic",
// documents
"pdf": "application-pdf",
"doc": "x-office-document",
"docx": "x-office-document",
"odt": "x-office-document",
"txt": "text-x-generic",
"md": "text-x-generic",
"xls": "x-office-spreadsheet",
"xlsx": "x-office-spreadsheet",
"ods": "x-office-spreadsheet",
"ppt": "x-office-presentation",
"pptx": "x-office-presentation",
"odp": "x-office-presentation",
// archives
"zip": "package-x-generic",
"tar": "package-x-generic",
"gz": "package-x-generic",
"xz": "package-x-generic",
"7z": "package-x-generic",
"rar": "package-x-generic",
// audio/video
"mp3": "audio-x-generic",
"flac": "audio-x-generic",
"wav": "audio-x-generic",
"ogg": "audio-x-generic",
"mp4": "video-x-generic",
"mkv": "video-x-generic",
"webm": "video-x-generic",
"avi": "video-x-generic",
"mov": "video-x-generic",
// code
"js": "text-x-script",
"ts": "text-x-script",
"py": "text-x-script",
"sh": "text-x-script",
"qml": "text-x-script",
"cpp": "text-x-c++src",
"c": "text-x-csrc",
"h": "text-x-chdr",
"json": "application-json",
"html": "text-html",
"css": "text-css"
})
function showInFolder(path: string): void {
const explorer = Config.general.apps.explorer;
let args = [];
if (explorer.includes("dolphin"))
args = [...explorer, "--select", path];
else
args = [...explorer, path];
if (Config.launcher.uwsm)
Quickshell.execDetached(["app2unit", "--", ...args]);
else
Quickshell.execDetached([...args]);
}
function launch(command: list<string>): void {
if (Config.launcher.uwsm)
Quickshell.execDetached(["app2unit", "--"].concat(command));
else
Quickshell.execDetached(command);
}
function transformSearch(search: string): string {
return search.slice(Config.launcher.actionPrefix.length + "files ".length);
}
function query(search: string): list<var> {
search = transformSearch(search);
if (search !== _pendingQuery) {
_pendingQuery = search;
debounceTimer.restart();
}
return results;
}
function _extOf(name: string): string {
const i = name.lastIndexOf(".");
// no dot, or dotfile with no real extension (".bashrc" -> treat as no ext)
if (i <= 0)
return "";
return name.slice(i + 1).toLowerCase();
}
function _iconFor(name: string, isDir: bool): string {
if (isDir)
return "folder";
const ext = _extOf(name);
return _extIcons[ext] ?? "text-x-generic";
}
function _buildEntry(path: string): var {
const name = path.split("/").pop();
const ext = _extOf(name);
// trailing slash from plocate's directory output (if -d/dir results ever appear)
const isDir = path.endsWith("/");
return {
path: path,
name: name,
icon: _iconFor(name, isDir),
isImage: !isDir && (_imageExts[ext] ?? false)
};
}
Timer {
id: debounceTimer
interval: 120
onTriggered: root._runQuery(root._pendingQuery)
}
function _runQuery(search: string): void {
_generation++;
if (!search) {
proc.running = false;
results = [];
return;
}
if (proc.running)
proc.running = false;
proc.buffer = [];
proc.myGeneration = _generation;
proc.command = ["plocate", "-i", "-b", "--limit", "50", search];
proc.running = true;
}
Process {
id: proc
property var buffer: []
property int myGeneration: 0
stdout: SplitParser {
onRead: line => proc.buffer.push(root._buildEntry(line))
}
onRunningChanged: {
if (!running && myGeneration === root._generation)
root.results = buffer;
}
}
}
@@ -0,0 +1,581 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import QtQuick.Layouts
import ZShell.Components
import ZShell.Config
import ZShell.Llm
import qs.Modules.Notifications.Sidebar.Chat.Content
import qs.Components
import qs.Services
Item {
id: root
property ChatSession chatData
// qmlformat off
readonly property var keys: [
Qt.Key_A,
Qt.Key_B,
Qt.Key_C,
Qt.Key_D,
Qt.Key_E,
Qt.Key_F,
Qt.Key_G,
Qt.Key_H,
Qt.Key_I,
Qt.Key_J,
Qt.Key_K,
Qt.Key_L,
Qt.Key_M,
Qt.Key_N,
Qt.Key_O,
Qt.Key_P,
Qt.Key_Q,
Qt.Key_R,
Qt.Key_S,
Qt.Key_T,
Qt.Key_U,
Qt.Key_V,
Qt.Key_W,
Qt.Key_X,
Qt.Key_Y,
Qt.Key_Z,
Qt.Key_Agrave,
Qt.Key_Aacute,
Qt.Key_Acircumflex,
Qt.Key_Atilde,
Qt.Key_Adiaeresis,
Qt.Key_Aring,
Qt.Key_AE,
Qt.Key_Ccedilla,
Qt.Key_Egrave,
Qt.Key_Eacute,
Qt.Key_Ecircumflex,
Qt.Key_Ediaeresis,
Qt.Key_Igrave,
Qt.Key_Iacute,
Qt.Key_Icircumflex,
Qt.Key_Idiaeresis,
Qt.Key_ETH,
Qt.Key_Ntilde,
Qt.Key_Ograve,
Qt.Key_Oacute,
Qt.Key_Ocircumflex,
Qt.Key_Otilde,
Qt.Key_Odiaeresis,
Qt.Key_Ooblique,
Qt.Key_Ugrave,
Qt.Key_Uacute,
Qt.Key_Ucircumflex,
Qt.Key_Udiaeresis,
Qt.Key_Yacute,
Qt.Key_ydiaeresis,
Qt.Key_THORN,
Qt.Key_ssharp
]
// qmlformat on
signal requestClose
readonly property bool atNewestEnd: !listLoader.item || listLoader.item.contentY < 4
property var pendingAttachments: []
function attachFiles(): void {
Files.pickFiles().then(files => {
if (!files || files.length === 0)
return;
const added = chatData.addAttachments(files);
root.pendingAttachments = root.pendingAttachments.concat(added);
});
}
function send(text: string): void {
if (text.trim() === "" && root.pendingAttachments.length === 0)
return;
root.scrollToLatest();
chatData.sendMessage(text, root.pendingAttachments);
input.text = "";
root.pendingAttachments = [];
}
function scrollToLatest(): void {
if (!listLoader.item || root.atNewestEnd)
return;
scrollAnim.stop();
scrollAnim.start();
}
function focusInput(): void {
Qt.callLater(() => input.forceActiveFocus());
}
onChatDataChanged: {
if (chatData)
focusInput();
}
Keys.onPressed: e => {
if (root.keys.includes(e.key)) {
input.insert(input.length, e.text);
focusInput();
}
}
Anim {
id: scrollAnim
target: listLoader.item
property: "contentY"
to: 0
type: Anim.DefaultEffects
onFinished: if (target)
target.contentY = 0
}
RowLayout {
id: header
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
spacing: Tokens.spacing.large
IconButton {
icon: "arrow_back"
inactiveColor: Colors.tPalette.m3surfaceContainerHigh
inactiveOnColor: Colors.palette.m3onSurfaceVariant
isRound: true
type: IconButton.Tonal
enabled: !ChatState.isWindow
visible: enabled
onClicked: root.requestClose()
}
CustomText {
Layout.fillWidth: ChatState.isWindow ? false : true
Layout.alignment: ChatState.isWindow ? Qt.AlignCenter : Qt.AlignLeft | Qt.AlignVCenter
elide: Text.ElideRight
font: Tokens.font.title.medium
text: qsTr(root.chatData?.title || ChatState.title)
}
}
Loader {
id: listLoader
anchors.bottom: input.top
anchors.bottomMargin: Tokens.spacing.medium
anchors.left: parent.left
anchors.right: parent.right
anchors.top: header.bottom
anchors.topMargin: Tokens.spacing.medium
active: root.chatData && root.chatData.loaded
sourceComponent: Flickable {
id: flick
anchors.fill: parent
clip: true
contentWidth: width
contentHeight: list.layoutHeight
transform: Rotation {
origin.y: flick.height / 2
origin.x: flick.width / 2
angle: 180
}
WheelInverter {
target: flick
}
Anim {
id: restoreAnim
target: flick
type: Anim.DefaultEffects
property: "contentY"
}
ChatListView {
id: list
width: flick.width
height: flick.height
yAnimDuration: Tokens.anim.durations.expressiveDefaultEffects
asynchronous: true
cacheBuffer: 50
estimatedHeight: 100
readyDelay: 5
removeDuration: Tokens.anim.durations.normal
spacing: 0
model: root.chatData.messagesModel
useCustomViewport: true
viewport: {
tWatcher.transform;
return Qt.rect(0, flick.contentY, width, flick.height);
}
onViewportAdjustNeeded: delta => {
restoreAnim.stop();
flick.contentY += delta;
}
onViewportRestoreRequested: target => {
restoreAnim.to = target;
restoreAnim.start();
}
delegate: MessageDelegate {
id: message
rootParent: flick
chatList: list
ChatListView.preferredHeight: implicitHeight
ChatListView.trackViewport: true
ChatListView.visibleHeight: ChatListView.removing ? 0 : implicitHeight
opacity: ChatListView.removing || ChatListView.adding ? 0 : 1
transform: Rotation {
origin.y: message.height / 2
origin.x: message.width / 2
angle: 180
}
Behavior on y {
enabled: message.ChatListView.ready
Anim {
id: yAnim
type: Anim.DefaultEffects
}
}
Behavior on x {
Anim {
type: Anim.DefaultEffects
}
}
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
}
}
CustomScrollBar.vertical: CustomScrollBar {
id: scrollBar
flickable: flick
parent: flick.parent
anchors.top: flick.top
anchors.right: flick.right
anchors.bottom: flick.bottom
inverted: true
transform: Rotation {
origin.y: flick.height / 2
origin.x: scrollBar.width / 2
angle: 180
axis {
y: 0
x: 1
z: 0
}
}
}
TransformWatcher {
id: tWatcher
a: flick.contentItem
b: list
}
}
Component.onCompleted: if (root.chatData)
root.chatData.messagesModel
}
EmptyBackground {
id: emptyState
anchors.fill: parent
spacing: Tokens.spacing.small
visible: root.chatData.messageCount < 1
}
Item {
anchors.bottom: input.top
anchors.bottomMargin: Tokens.spacing.extraLarge
anchors.horizontalCenter: parent.horizontalCenter
implicitHeight: scrollToBottom.implicitHeight
implicitWidth: scrollToBottom.implicitWidth
scale: root.atNewestEnd || !listLoader.item || listLoader.item.contentHeight <= listLoader.item.height ? 0 : 1
visible: scale > 0
Behavior on scale {
Anim {}
}
Elevation {
anchors.fill: parent
level: 2
radius: scrollToBottom.radius
}
IconButton {
id: scrollToBottom
anchors.centerIn: parent
font: Tokens.font.icon.large
icon: "arrow_downward"
isRound: true
padding: Tokens.padding.extraSmall
type: IconButton.Tonal
onClicked: {
root.scrollToLatest();
}
}
}
GridView {
id: attachments
readonly property int columns: ChatState.isWindow ? 5 : 3
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: input.top
anchors.bottomMargin: Tokens.spacing.medium
flow: GridView.LeftToRight
verticalLayoutDirection: GridView.BottomToTop
cellWidth: width / columns
cellHeight: cellWidth
implicitHeight: Math.ceil(count / columns) * cellHeight
displaced: Transition {
Anim {
property: "x"
}
Anim {
property: "y"
}
}
add: Transition {
id: addTrans
Anim {
property: "y"
from: addTrans.ViewTransition.destination.y + attachments.cellHeight / 4
}
Anim {
property: "opacity"
from: 0
to: 1
}
}
remove: Transition {
id: removeTrans
Anim {
property: "y"
to: removeTrans.ViewTransition.destination.y + attachments.cellHeight / 4
}
Anim {
property: "opacity"
from: 1
to: 0
}
}
model: ScriptModel {
objectProp: "path"
values: root.pendingAttachments
}
delegate: Item {
id: thumb
required property var modelData
function fileIcon(mime): string {
if (mime.startsWith("archive/") || mime === "application/zip")
return "folder_zip";
if (mime === "application/pdf")
return "picture_as_pdf";
if (mime.startsWith("audio/"))
return "audio_file";
if (mime.startsWith("video/"))
return "video_file";
if (mime.startsWith("text/"))
return "description";
return "insert_drive_file";
}
width: attachments.cellWidth
height: width
opacity: modelData ? 1 : 0
CustomClippingRect {
anchors.fill: parent
anchors.margins: Tokens.spacing.extraSmall
radius: Tokens.rounding.medium
color: Colors.tPalette.m3surfaceContainer
FadeImage {
id: attImage
anchors.fill: parent
fillMode: FadeImage.PreserveAspectCrop
asynchronous: true
source: visible ? thumb.modelData?.path ?? "" : ""
visible: thumb.modelData?.isImage ?? false
}
MaterialIcon {
anchors.centerIn: parent
fontStyle: Tokens.font.icon.large
text: thumb.fileIcon(thumb.modelData?.mimeType ?? "")
visible: !thumb.modelData?.isImage ?? false
}
Loader {
id: attIndicatorLoader
anchors.centerIn: parent
opacity: 0
active: opacity > 0 && (thumb.modelData?.isImage ?? false)
sourceComponent: CustomRect {
implicitWidth: attLoadingIndicator.implicitSize + Tokens.padding.largeIncreased * 2
implicitHeight: attLoadingIndicator.implicitSize + Tokens.padding.largeIncreased * 2
color: Colors.palette.m3primaryContainer
radius: Tokens.rounding.full
LoadingIndicator {
id: attLoadingIndicator
anchors.centerIn: parent
containsIcon: true
implicitSize: thumb.implicitWidth * 0.4
}
}
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
}
Timer {
id: attLoadDebounceTimer
interval: 100
onTriggered: {
if (attImage.status !== Image.Ready)
attIndicatorLoader.opacity = 1;
}
}
MouseArea {
id: hover
anchors.fill: parent
hoverEnabled: true
cursorShape: undefined
IconButton {
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Tokens.padding.medium
icon: "close"
visible: opacity > 0
enabled: thumb.opacity > 0
opacity: hover.containsMouse ? 1 : 0
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
onClicked: root.pendingAttachments = root.pendingAttachments.filter(a => a !== thumb.modelData)
}
}
}
}
}
ChatInput {
id: input
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Tokens.padding.medium
bg.border.color: Colors.palette.m3outlineVariant
bg.color: Colors.tPalette.m3surfaceContainerLowest
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: 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();
}
Keys.onPressed: e => {
if (e.key == Qt.Key_Return) {
if (!(e.modifiers & Qt.ShiftModifier)) {
root.send(text);
e.accepted = true;
}
}
if (e.key == Qt.Key_V && e.modifiers & Qt.ControlModifier) {
if (root.chatData.clipboardHasAttachment()) {
const att = root.chatData.pasteAttachments();
root.pendingAttachments = [...root.pendingAttachments, ...att];
e.accepted = true;
}
}
}
onButtonPressed: {
if (LlmClient.busy)
LlmClient.stop();
else
root.send(text);
}
}
}
@@ -0,0 +1,389 @@
import QtQuick
import ZShell.Components
import ZShell.Config
import ZShell.Llm
import qs.Effects
import qs.Components
import qs.Services
CustomClippingRect {
id: root
property bool expanded: false
property bool highlighted: false
property bool slim: false
required property ChatSession modelData
signal clicked(content: ChatSession)
signal remove(content: ChatSession)
color: highlighted ? Qt.alpha(Colors.palette.m3primaryContainer, 0.3) : Qt.alpha(Colors.tPalette.m3surfaceContainer, 0.2)
implicitHeight: {
let h = 0;
if (slim)
return chatIcon.implicitHeight + chatIcon.anchors.topMargin * 2;
h += infoContainer.implicitHeight;
if (expanded)
h += body.implicitHeight + body.topMargin + actionRow.implicitHeight + actionRow.anchors.topMargin;
else
h += preview.implicitHeight + preview.topMargin;
h += Tokens.padding.small;
const icon = chatIcon.implicitHeight + chatIcon.anchors.topMargin * 2;
if (h < icon)
return icon;
return h;
}
radius: Tokens.rounding.large
clip: true
CustomRect {
id: borderHighlight
anchors.fill: parent
border.width: root.highlighted ? 2 : 0
border.color: Colors.palette.m3secondary
radius: root.radius - border.width + 1
Behavior on border.width {
Anim {}
}
}
Behavior on implicitHeight {
enabled: titleText.readOnly
Anim {}
}
MaterialIcon {
id: chatIcon
anchors.left: parent.left
anchors.leftMargin: Tokens.padding.large
anchors.top: parent.top
anchors.topMargin: Tokens.padding.large
fontStyle: Tokens.font.icon.extraLarge
text: root.modelData.icon || "check"
color: Colors.palette.m3onSurface
}
Item {
id: infoContainer
anchors.left: chatIcon.right
anchors.leftMargin: Tokens.spacing.medium
anchors.top: parent.top
anchors.right: expandBtn.left
anchors.rightMargin: Tokens.spacing.medium
implicitHeight: {
let h = 0;
h += title.implicitHeight + title.anchors.topMargin;
h += timestamp.implicitHeight + timestamp.anchors.topMargin;
if (root.expanded)
h += created.implicitHeight + created.anchors.topMargin;
return h;
}
opacity: root.slim ? 0 : 1
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
CustomRect {
id: title
anchors.left: parent.left
anchors.topMargin: Tokens.padding.extraSmall
anchors.top: parent.top
color: Colors.layer(Colors.palette.m3surfaceContainerHighest, 1)
implicitHeight: titleText.implicitHeight + Tokens.padding.extraSmall * 2
radius: Tokens.rounding.full
implicitWidth: titleText.implicitWidth + Tokens.padding.large * 2
opacity: titleText.readOnly ? 0 : 1
Behavior on opacity {
Anim {}
}
}
Item {
id: titleWrapper
anchors.left: parent.left
anchors.top: parent.top
anchors.right: parent.right
implicitHeight: titleText.implicitHeight + Tokens.padding.small * 2
layer.enabled: true
layer.effect: OpacityMask {
maskSource: fadeMask
}
TextFieldBase {
id: titleText
anchors.left: parent.left
anchors.leftMargin: readOnly ? 0 : Tokens.padding.large
anchors.top: parent.top
anchors.topMargin: readOnly ? Tokens.padding.small : Tokens.padding.extraSmall * 2
font: Tokens.font.body.medium
text: root.modelData.title || qsTr("New chat")
readOnly: true
color: Colors.palette.m3onSurface
Behavior on anchors.leftMargin {
Anim {}
}
Behavior on anchors.topMargin {
Anim {}
}
onEditingFinished: root.modelData.title = text
onReadOnlyChanged: {
if (!readOnly) {
this.forceActiveFocus();
cursorPosition = text.length;
} else {
root.forceActiveFocus();
}
}
}
}
CustomRect {
id: fadeMask
anchors.fill: titleWrapper
layer.enabled: true
visible: false
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop {
color: Qt.rgba(1, 1, 1, 1.0)
position: 0.85
}
GradientStop {
color: Qt.rgba(1, 1, 1, 0)
position: 1.0
}
}
}
CustomText {
id: created
anchors.top: title.bottom
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
Behavior on opacity {
Anim {}
}
}
CustomText {
id: updated
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
Behavior on opacity {
Anim {}
}
}
CustomText {
id: timestamp
anchors.left: parent.left
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 {
name: "expanded"
when: root.expanded
AnchorChanges {
target: timestamp
anchors.left: updated.right
anchors.top: undefined
anchors.verticalCenter: updated.verticalCenter
}
}
transitions: Transition {
AnchorAnim {}
}
}
}
CustomText {
id: preview
readonly property int topMargin: Tokens.spacing.extraSmall
anchors.left: chatIcon.right
anchors.margins: Tokens.spacing.medium
anchors.right: parent.right
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?.content.replace(/\s+/g, " ").trim() ?? qsTr("No messages yet")
Behavior on y {
Anim {}
}
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
}
CustomText {
id: body
readonly property int topMargin: root.expanded ? Tokens.spacing.medium : Tokens.spacing.extraSmall
anchors.left: chatIcon.right
anchors.margins: Tokens.spacing.medium
anchors.right: parent.right
y: infoContainer.implicitHeight + topMargin
animate: true
color: Colors.palette.m3onSurfaceVariant
maximumLineCount: 5
opacity: root.expanded ? 1 : 0
text: root.modelData.messagesModel.lastMessage?.content ?? qsTr("No messages yet")
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
Behavior on y {
Anim {}
}
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
}
StateLayer {
id: sLayer
property int startY
onClicked: {
root.clicked(root.modelData);
}
}
ButtonRow {
id: actionRow
anchors.top: body.bottom
anchors.left: chatIcon.right
anchors.right: parent.right
anchors.topMargin: Tokens.spacing.small
anchors.leftMargin: Tokens.spacing.medium
anchors.rightMargin: Tokens.padding.medium
spacing: Tokens.spacing.medium
IconButton {
icon: "close"
fillWidth: true
shapeMorph: true
isRound: true
font: Tokens.font.icon.medium
inactiveColor: Colors.layer(Colors.palette.m3surfaceContainerHighest, 3)
inactiveOnColor: Colors.palette.m3onSurfaceVariant
onClicked: root.remove(root.modelData)
}
IconButton {
icon: titleText.readOnly ? "edit" : "check"
label.animate: true
isToggle: true
fillWidth: true
shapeMorph: true
font: Tokens.font.icon.medium
isRound: true
inactiveColor: Colors.layer(Colors.palette.m3surfaceContainerHighest, 3)
inactiveOnColor: Colors.palette.m3onSurfaceVariant
activeColor: Colors.palette.m3secondary
activeOnColor: Colors.palette.m3onSecondary
checked: !titleText.readOnly
onClicked: titleText.readOnly = !titleText.readOnly
}
}
CustomRect {
id: expandBtn
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Tokens.padding.small
implicitHeight: expandIcon.implicitHeight
implicitWidth: expandIcon.implicitHeight
radius: Tokens.rounding.full
color: Colors.layer(Colors.palette.m3surfaceContainerHighest, 3)
opacity: root.slim ? 0 : 1
visible: opacity > 0
enabled: visible
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
StateLayer {
onClicked: root.expanded = !root.expanded
}
MaterialIcon {
id: expandIcon
anchors.centerIn: parent
anchors.verticalCenterOffset: root.expanded ? -1 : 1
rotation: root.expanded ? 180 : 0
color: Colors.palette.m3onSurface
text: "expand_more"
Behavior on anchors.verticalCenterOffset {
Anim {}
}
Behavior on rotation {
Anim {}
}
}
}
}
@@ -0,0 +1,142 @@
import QtQuick
import ZShell.Config
import qs.Components
import qs.Services
TextAreaBase {
id: root
readonly property alias bg: bg
readonly property alias sendIcon: sendIcon
readonly property alias glow: glow
property bool fastGlow: false
signal buttonPressed
bottomPadding: Tokens.padding.large
leftPadding: Tokens.padding.extraLarge
rightPadding: sendIcon.width + sendIcon.anchors.rightMargin + Tokens.spacing.small
topPadding: Tokens.padding.large
wrapMode: TextAreaBase.Wrap
onFastGlowChanged: {
if (fastGlow) {
resetAnim.stop();
boostAnim.restart();
} else {
boostAnim.stop();
resetAnim.restart();
}
}
background: CustomRect {
id: bg
anchors.fill: parent
color: Colors.tPalette.m3surfaceContainer
radius: Tokens.rounding.extraLarge
GeminiGlow {
id: glow
glowSize: 32
z: 5
opacity: root.activeFocus ? 1 : 0
visible: opacity > 0
Behavior on opacity {
Anim {}
}
SequentialAnimation {
id: boostAnim
NumberAnimation {
target: glow
property: "speed"
to: 4.0
duration: 250
easing.type: Easing.OutCubic
}
NumberAnimation {
target: glow
property: "speed"
to: 2.25
duration: 1500
easing.type: Easing.InOutQuad
}
}
// Ease back down to normal once fastGlow is false
NumberAnimation {
id: resetAnim
target: glow
property: "speed"
to: 1.0
duration: 800
easing.type: Easing.InOutQuad
}
}
StateLayer {
id: stateLayer
cursorShape: Qt.IBeamCursor
enabled: !root.activeFocus
manualPressOverride: tapHandler.pressed
onClicked: root.forceActiveFocus()
}
}
onPressed: {
if (stateLayer.enabled)
stateLayer.press(stateLayer.mouseX, stateLayer.mouseY);
}
CustomText {
id: placeholder
anchors.left: parent.left
anchors.leftMargin: root.leftPadding
anchors.verticalCenter: parent.verticalCenter
color: root.placeholderTextColor
font: root.font
opacity: root.text ? 0 : 1
text: root.placeholderText
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
}
IconButton {
id: sendIcon
anchors.right: parent.right
anchors.rightMargin: Tokens.padding.small
anchors.verticalCenter: parent.verticalCenter
enabled: root.text
icon: "upward_arrow"
opacity: root.text ? 1 : 0
radius: Tokens.rounding.full
radiusMorph: false
stateLayer.hoverEnabled: enabled
type: IconButton.Text
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
onClicked: root.buttonPressed()
}
TapHandler {
id: tapHandler
}
}
@@ -0,0 +1,306 @@
pragma ComponentBehavior: Bound
import Quickshell
import QtQuick
import QtQuick.Layouts
import ZShell.Llm
import ZShell.Config
import qs.Modules.Notifications.Sidebar.Chat.Content
import qs.Services
import qs.Components
Item {
id: root
property alias list: list
property alias model: list.model
property bool highlight: false
property bool slim: false
signal requestExpand
signal deleteChatRequest(content: ChatSession)
signal loadChatRequest(content: ChatSession, index: int)
signal newChatRequest
onSlimChanged: {
if (slim)
modelsContainer.open = false;
}
CustomText {
id: chatHeader
anchors.left: parent.left
anchors.margins: Tokens.padding.extraSmall
anchors.right: parent.right
anchors.top: parent.top
color: Colors.palette.m3outline
font.family: "CaskaydiaCove NF"
font.pointSize: 13
font.weight: 500
text: qsTr("%1 Chat%2").arg(list.count).arg(list.count > 1 ? "s" : "")
}
CustomListView {
id: list
anchors.bottom: modelsContainer.top
anchors.left: parent.left
anchors.right: parent.right
anchors.top: chatHeader.bottom
anchors.topMargin: Tokens.spacing.medium
clip: true
spacing: Tokens.spacing.medium
add: Transition {
Anim {
from: list.width
property: "x"
}
}
remove: Transition {
Anim {
to: list.width
property: "x"
}
Anim {
to: 0
property: "opacity"
}
}
displaced: Transition {
Anim {
property: "y"
}
}
move: Transition {
Anim {
property: "y"
}
}
delegate: ChatDelegate {
id: chat
required property int index
implicitWidth: ListView.view.width
highlighted: root.highlight && ChatState.chatSession === modelData
slim: root.slim
onHighlightedChanged: if (highlighted)
list.currentIndex = index
onClicked: content => root.loadChatRequest(content, index)
onRemove: content => root.deleteChatRequest(content)
}
TapHandler {
onTapped: {
root.focus = true;
ChatState.fabExpanded = false;
modelsContainer.expanded = false;
}
}
}
ModelsContainer {
id: modelsContainer
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: fabRoot.left
anchors.margins: Tokens.padding.small
anchors.rightMargin: Tokens.spacing.small
rootParent: root
fabRoot: fabRoot
opacity: root.slim ? 0 : 1
visible: opacity > 0
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
}
IconButton {
id: fabRoot
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.margins: Tokens.padding.small
padding: 8
font.pointSize: Math.round(18 * 1.2)
icon: root.slim ? "left_panel_open" : "add"
isRound: ChatState.fabExpanded
label.transform: Rotation {
origin.y: fabRoot.label.height / 2
origin.x: fabRoot.label.width / 2
angle: ChatState.fabExpanded && (ChatState.narrowSidebarExpanded && !ChatState.isWide || !ChatState.isWindow) ? 135 : 0
Behavior on angle {
Anim {}
}
}
onClicked: {
if (root.slim && !ChatState.narrowSidebarExpanded) {
root.requestExpand();
return;
}
ChatState.fabExpanded = !ChatState.fabExpanded;
}
Elevation {
anchors.fill: parent
level: fabRoot.stateLayer.containsMouse ? 4 : 3
radius: parent.radius
z: -1
}
}
ColumnLayout {
id: fabMenu
anchors.bottom: fabRoot.top
anchors.right: fabRoot.right
anchors.bottomMargin: Tokens.padding.medium
Repeater {
id: fabRep
model: ListModel {
ListElement {
name: "new chat"
icon: "add"
}
ListElement {
name: "pop out in window"
icon: "open_in_new"
}
}
CustomClippingRect {
id: fabMenuItem
required property var modelData
required property int index
Layout.alignment: Qt.AlignRight
implicitHeight: fabMenuItemInner.implicitHeight + Tokens.padding.medium * 2
radius: sLayer.pressed ? Tokens.rounding.small : implicitHeight / 2
color: Colors.palette.m3primaryContainer
visible: !(modelData.name === "pop out in window" && ChatState.isWindow)
Behavior on radius {
Anim {
type: Anim.DefaultEffects
}
}
opacity: 0
states: State {
name: "visible"
when: ChatState.fabExpanded
PropertyChanges {
fabMenuItem.implicitWidth: fabMenuItemInner.implicitWidth + Tokens.padding.large * 2
fabMenuItem.opacity: 1
fabMenuItemInner.opacity: 1
}
}
transitions: [
Transition {
to: "visible"
SequentialAnimation {
PauseAnimation {
duration: (fabRep.count - 1 - fabMenuItem.index) * 50
}
ParallelAnimation {
Anim {
property: "implicitWidth"
type: Anim.FastSpatial
}
Anim {
property: "opacity"
duration: Tokens.anim.durations.small
easing: Tokens.anim.standard
}
}
}
},
Transition {
from: "visible"
SequentialAnimation {
PauseAnimation {
duration: fabMenuItem.index * Tokens.anim.durations.small / 8
}
ParallelAnimation {
Anim {
property: "implicitWidth"
type: Anim.FastSpatial
}
Anim {
property: "opacity"
duration: Tokens.anim.durations.small
easing: Tokens.anim.standard
}
}
}
}
]
RowLayout {
id: fabMenuItemInner
anchors.centerIn: parent
spacing: Tokens.spacing.medium
opacity: 0
MaterialIcon {
text: fabMenuItem.modelData.icon
color: Colors.palette.m3onPrimaryContainer
fontStyle: Tokens.font.icon.large
fill: 1
}
CustomText {
animate: true
text: fabMenuItem.modelData.name
font.capitalization: Font.Capitalize
color: Colors.palette.m3onPrimaryContainer
Layout.preferredWidth: implicitWidth
Behavior on Layout.preferredWidth {
Anim {}
}
}
}
StateLayer {
id: sLayer
onClicked: {
const name = fabMenuItem.modelData.name;
if (name === "new chat")
root.newChatRequest();
else if (name === "pop out in window")
Detach.create();
ChatState.fabExpanded = false;
}
}
}
}
}
}
@@ -0,0 +1,83 @@
pragma ComponentBehavior: Bound
import ZShell.Config
import ZShell.Llm
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Helpers
import qs.Modules.Notifications.Sidebar
import qs.Components
import qs.Services
Item {
id: root
anchors.fill: parent
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);
} else
stack.push(chatList);
}
function openChat(): void {
if (stack.depth < 2)
stack.push(chatContent);
}
StackView {
id: stack
anchors.fill: parent
}
Component {
id: chatList
ChatList {
model: LlmClient.chats
onDeleteChatRequest: chat => {
const wasCurrent = chat === ChatState.chatSession;
LlmClient.chats.remove(chat);
if (wasCurrent)
ChatState.openDraft();
}
onLoadChatRequest: (chat, index) => {
ChatState.chatSession = chat;
openChat();
ChatState.inChat = true;
}
onNewChatRequest: {
ChatState.openDraft();
openChat();
ChatState.inChat = true;
}
}
}
Component {
id: chatContent
ChatContent {
chatData: ChatState.chatSession
onRequestClose: {
stack.pop();
ChatState.inChat = false;
ChatState.openDraft();
}
}
}
}
@@ -0,0 +1,33 @@
pragma Singleton
import QtQuick
import Quickshell
import ZShell.Llm
import qs.Services
Singleton {
id: root
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
property bool isWide: false
property bool settledSlim: false
property bool settledWide: false
property bool narrowSidebarExpanded: false
property bool windowIsWide: false
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
}
@@ -0,0 +1,118 @@
import QtQuick
import Quickshell
import ZShell.Config
import ZShell.Components
import ZShell.Llm
import qs.Components
import qs.Services
Item {
id: root
required property TextEdit edit
required property bool hovered
required property bool isUser
required property ChatMessage message
required property bool running
implicitHeight: editButton.implicitHeight
implicitWidth: generationTools.implicitWidth + editTools.implicitWidth + Tokens.spacing.small
ButtonRow {
id: generationTools
enabled: visible
visible: !root.isUser
IconButton {
enabled: root.message.generationCount > 1 && root.message.activeGenerationIndex !== 0
icon: "chevron_left"
type: IconButton.Tonal
isRound: true
onClicked: {
if (!root.edit.readOnly)
root.edit.readOnly = true;
root.message.setActiveGeneration(root.message.activeGenerationIndex - 1);
}
}
IconButton {
enabled: false
icon: `${root.message.activeGenerationIndex + 1}`
label.font: Tokens.font.body.small
type: IconButton.Text
}
IconButton {
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)
root.edit.readOnly = true;
root.message.setActiveGeneration(root.message.activeGenerationIndex + 1);
}
}
}
ButtonRow {
id: editTools
anchors.right: parent.right
opacity: !root.running && root.hovered ? 1 : 0
spacing: Tokens.spacing.small
visible: opacity > 0
Behavior on opacity {
Anim {}
}
IconButton {
icon: "refresh"
inactiveColor: Colors.palette.m3secondary
inactiveOnColor: Colors.palette.m3onSecondary
scale: root.edit.readOnly ? 1 : 0
visible: !root.isUser
isRound: true
Behavior on scale {
Anim {}
}
onClicked: {
root.message.retry();
}
}
IconButton {
icon: "content_copy"
inactiveColor: Colors.palette.m3secondary
inactiveOnColor: Colors.palette.m3onSecondary
scale: root.edit.readOnly ? 1 : 0
isRound: true
Behavior on scale {
Anim {}
}
onClicked: Quickshell.clipboardText = root.message.content
}
IconButton {
id: editButton
icon: root.edit.readOnly ? "edit" : "check"
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()
}
}
}
}
}
@@ -0,0 +1,27 @@
import Quickshell
import QtQuick
import ZShell.Config
import qs.Components
import qs.Services
TextEditBase {
id: root
color: Colors.palette.m3onSurface
readOnly: true
textFormat: Text.MarkdownText
font: Tokens.font.body.small
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
onLinkActivated: link => {
Qt.openUrlExternally(link);
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: root.hoveredLink !== "" ? Qt.PointingHandCursor : Qt.IBeamCursor
preventStealing: false
hoverEnabled: true
}
}
@@ -0,0 +1,147 @@
import QtQuick
import ZShell.Llm
import ZShell.Config
import qs.Components
import qs.Services
import qs.Modules.Notifications.Sidebar.Chat
Item {
id: root
property int animOff
property Item currentItem
property bool noAnim: false
property bool loading: false
property int lastIdx: -1
readonly property Component conversationComp: ChatContent {}
signal requestClose
function loadConversation(chat): void {
if (currentItem) {
currentItem.destroy();
currentItem = null;
}
if (!chat)
return;
root.loading = true;
const incubator = root.conversationComp.incubateObject(container, {
chatData: chat
});
const attach = () => {
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)
enterAnim.start();
};
if (incubator.status === Component.Ready)
attach();
else
incubator.onStatusChanged = status => {
if (status === Component.Ready)
attach();
};
}
Item {
id: container
anchors.fill: parent
layer.enabled: opacity < 1
objectName: "ConversationContainer"
Component.onCompleted: {
if (ChatState.chatSession)
root.loadConversation(ChatState.chatSession);
}
}
LoadingIndicator {
anchors.centerIn: parent
implicitSize: Tokens.font.icon.extraLarge.pointSize * 4
opacity: root.loading ? 1 : 0
visible: opacity > 0
Behavior on opacity {
Anim {}
}
}
Connections {
function onChatSessionChanged(): void {
exitAnim.complete();
enterAnim.complete();
if (root.noAnim) {
root.loadConversation(ChatState.chatSession);
return;
}
root.animOff = Tokens.padding.small * (ChatState.currentIdx > root.lastIdx ? 1 : -1);
root.lastIdx = ChatState.currentIdx;
exitAnim.start();
}
target: ChatState
}
SequentialAnimation {
id: exitAnim
Anim {
property: "opacity"
target: container
to: 0
type: Anim.DefaultEffects
}
ScriptAction {
script: root.loadConversation(ChatState.chatSession)
}
}
SequentialAnimation {
id: enterAnim
PropertyAction {
property: "topMargin"
target: container.anchors
value: root.animOff
}
PropertyAction {
property: "bottomMargin"
target: container.anchors
value: -root.animOff
}
ParallelAnimation {
Anim {
from: 0
property: "opacity"
target: container
to: 1
type: Anim.SlowEffects
}
Anim {
properties: "topMargin,bottomMargin"
target: container.anchors
to: 0
type: Anim.SlowEffects
}
}
}
}
@@ -0,0 +1,296 @@
import QtQuick
import QtQuick.Effects
import QtQuick.Shapes
import QtQuick.Layouts
import Quickshell
import ZShell.Config
import ZShell.Llm
import qs.Components
import qs.Services
CustomRect {
id: root
required property string language
required property string code
property bool copied: false
property color codeBackgroundColor: Colors.palette.m3surfaceContainerHigh
property color codeHeaderColor: Colors.palette.m3outline
property string html: ""
property bool colored: false
property string requestedCode: ""
property string requestedLanguage: ""
property int highlightToken: 0
function refresh() {
requestedCode = root.code;
requestedLanguage = root.language;
const token = ++root.highlightToken;
CodeHighlighter.highlight(root.code, root.language, root, token);
}
function onHighlightSpans(token, spans) {
if (token !== root.highlightToken)
return;
if (root.code !== root.requestedCode || root.language !== root.requestedLanguage)
return;
html = spans.length ? root.highlightedHtml(root.code, spans) : "";
colored = root.visible && html.length > 0;
}
function roleColor(kind) {
var s = CodeColors.active;
switch (kind) {
case "comment":
return s.comment;
case "string":
return s.string;
case "string.key":
return s.stringKey;
case "number":
case "constant":
return s.number;
case "keyword":
return s.keyword;
case "type":
return s.type;
case "function":
return s.functions;
case "method":
return s.method ?? s.functions;
case "macro":
return s.macro;
case "preproc":
return s.preproc ?? s.macro;
case "operator":
return s.operator ?? s.normal;
case "property":
return s.property ?? s.normal;
case "label":
return s.label;
case "attribute":
return s.attribute ?? s.label;
default:
return s.normal;
}
}
function escapeHtml(text): string {
return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
// Mirrors the whitespace rules of the rich text rendering below (leading
// whitespace kept, mid-line runs collapsed, tabs as single spaces) so the
// plain and colored layouts are identical.
function plainForDisplay(code): string {
return code.split("\n").map(line => {
const i = line.search(/\S/);
if (i < 0)
return "";
const lead = line.slice(0, i).replace(/\t/g, " ");
return lead + line.slice(i).replace(/\s+/g, " ").replace(/\s+$/, "");
}).join("\n");
}
function highlightedHtml(code, spans): string {
let out;
if (!spans.length) {
out = escapeHtml(code);
} else {
out = "";
let pos = 0;
for (let i = 0; i < spans.length; i++) {
const span = spans[i];
const start = Math.min(span.start, code.length);
const end = Math.min(span.start + span.length, code.length);
if (end <= pos)
continue;
if (start > pos)
out += escapeHtml(code.slice(pos, start));
out += `<font color="${roleColor(span.kind)}">` + escapeHtml(code.slice(start, end)) + "</font>";
pos = end;
}
if (pos < code.length)
out += escapeHtml(code.slice(pos));
}
return out.replace(/(^|\n)[ \t]+/g, ws => ws.replace(/[ \t]/g, "&nbsp;")).replace(/\n/g, "<br>");
}
implicitWidth: headerRow.implicitWidth + headerRow.anchors.leftMargin + headerRow.anchors.rightMargin
implicitHeight: headerRow.anchors.topMargin + headerRow.implicitHeight + codeRect.implicitHeight + codeRect.anchors.margins + codeRect.anchors.topMargin
color: root.codeBackgroundColor
radius: Tokens.rounding.medium
onLanguageChanged: {
html = "";
colored = false;
if (root.visible)
refreshTimer.restart();
}
onCodeChanged: {
colored = false;
if (root.visible)
refreshTimer.restart();
}
onVisibleChanged: if (root.visible)
refreshTimer.restart()
Timer {
id: refreshTimer
interval: 200
repeat: false
onTriggered: refresh()
}
RowLayout {
id: headerRow
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: Tokens.padding.small
anchors.leftMargin: Tokens.padding.small
anchors.rightMargin: Tokens.padding.small
spacing: Tokens.spacing.small
Item {
id: iconItem
readonly property string iconPath: CodeIcons.path(root.language)
Layout.fillHeight: true
implicitWidth: height
Shape {
id: shape
anchors.centerIn: parent
visible: iconItem.iconPath !== ""
preferredRendererType: Shape.CurveRenderer
scale: Math.min(langText.implicitHeight / height, langText.implicitHeight / width)
ShapePath {
strokeColor: "transparent"
fillColor: Colors.palette.m3tertiary
fillRule: ShapePath.WindingFill
PathSvg {
path: iconItem.iconPath
}
}
}
Shape {
id: fallbackShape
anchors.centerIn: parent
anchors.verticalCenterOffset: -1
visible: iconItem.iconPath === ""
preferredRendererType: Shape.CurveRenderer
scale: Math.min(langText.implicitHeight / height, langText.implicitHeight / width)
ShapePath {
strokeColor: Colors.palette.m3tertiary
strokeWidth: 16
capStyle: ShapePath.RoundCap
joinStyle: ShapePath.RoundJoin
fillColor: "transparent"
PathSvg {
path: "M 40 64 L 112 128 L 40 192"
}
}
ShapePath {
strokeColor: Colors.palette.m3tertiary
strokeWidth: 16
capStyle: ShapePath.RoundCap
fillColor: "transparent"
PathSvg {
path: "M 120 192 L 216 192"
}
}
}
}
CustomText {
id: langText
Layout.alignment: Qt.AlignVCenter
color: root.codeHeaderColor
font: Tokens.font.label.small
text: CodeIcons.name(root.language)
visible: root.language.length > 0
}
Item {
Layout.fillWidth: true
}
IconButton {
icon: root.copied ? "check" : "content_copy"
inactiveColor: "transparent"
inactiveOnColor: root.codeHeaderColor
label.animate: true
type: IconButton.Text
onClicked: {
Quickshell.clipboardText = root.code;
root.copied = true;
copyResetTimer.restart();
}
Timer {
id: copyResetTimer
interval: 1500
onTriggered: root.copied = false
}
}
}
CustomClippingRect {
id: codeRect
anchors.left: parent.left
anchors.top: headerRow.bottom
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.topMargin: Tokens.padding.small
radius: root.radius - anchors.margins
anchors.margins: Tokens.padding.extraSmall
implicitWidth: codeText.implicitWidth + codeFlick.anchors.margins * 2
implicitHeight: codeText.implicitHeight + codeFlick.anchors.margins * 2
color: CodeColors.active.bg
Flickable {
id: codeFlick
anchors.fill: parent
anchors.margins: Tokens.padding.small
CustomScrollBar.horizontal: CustomScrollBar {
flickable: codeFlick
parent: codeFlick.parent
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
}
TextAreaBase.flickable: TextAreaBase {
id: codeText
color: CodeColors.active.normal
layer.enabled: true
clip: false
font: Tokens.font.mono.small
textFormat: root.colored ? Text.RichText : Text.PlainText
text: root.colored ? root.html : root.plainForDisplay(root.code)
readOnly: true
}
}
}
}
@@ -0,0 +1,231 @@
pragma Singleton
import Quickshell
import Quickshell.Io
import QtQuick
import ZShell.Config
Singleton {
id: root
readonly property JsonObject schemes: JsonObject {
readonly property Scheme oneDark: Scheme {
comment: "#5c6370"
string: "#98c379"
stringKey: "#e06c75"
number: "#d19a66"
keyword: "#c678dd"
type: "#e5c07b"
functions: "#61afef"
method: "#61afef"
macro: "#98c379"
preproc: "#abb2bf"
operator: "#abb2bf"
property: "#abb2bf"
label: "#e06c75"
attribute: "#d19a66"
bg: "#282c34"
normal: "#abb2bf"
}
readonly property Scheme nord: Scheme {
comment: "#4c566a"
string: "#a3be8c"
stringKey: "#ebcb8b"
number: "#b48ead"
keyword: "#81a1c1"
type: "#8fbcbb"
functions: "#88c0d0"
method: "#88c0d0"
macro: "#5e81ac"
preproc: "#5e81ac"
operator: "#81a1c1"
property: "#d8dee9"
label: "#d08770"
attribute: "#d8dee9"
bg: "#2e3440"
normal: "#d8dee9"
}
readonly property Scheme dracula: Scheme {
comment: "#6272a4"
string: "#f1fa8c"
stringKey: "#f8f8f2"
number: "#ffb86c"
keyword: "#ff79c6"
type: "#8be9fd"
functions: "#50fa7b"
method: "#50fa7b"
macro: "#ff79c6"
preproc: "#ff79c6"
operator: "#f8f8f2"
property: "#f8f8f2"
label: "#6272a4"
attribute: "#8be9fd"
bg: "#282a36"
normal: "#f8f8f2"
}
readonly property Scheme githubDark: Scheme {
comment: "#8b949e"
string: "#a5d6ff"
stringKey: "#79c0ff"
number: "#79c0ff"
keyword: "#ff7b72"
type: "#ffa657"
functions: "#d2a8ff"
method: "#d2a8ff"
macro: "#ff7b72"
preproc: "#79c0ff"
operator: "#ff7b72"
property: "#79c0ff"
label: "#7ee787"
attribute: "#7ee787"
bg: "#0d1117"
normal: "#c9d1d9"
}
readonly property Scheme solarizedDark: Scheme {
comment: "#586e75"
string: "#2aa198"
stringKey: "#268bd2"
number: "#2aa198"
keyword: "#859900"
type: "#b58900"
functions: "#268bd2"
method: "#268bd2"
macro: "#cb4b16"
preproc: "#cb4b16"
operator: "#859900"
property: "#268bd2"
label: "#6c71c4"
attribute: "#657b83"
bg: "#002b36"
normal: "#839496"
}
readonly property Scheme monokai: Scheme {
comment: "#75715e"
string: "#e6db74"
stringKey: "#f8f8f2"
number: "#ae81ff"
keyword: "#f92672"
type: "#a6e22e"
functions: "#a6e22e"
method: "#a6e22e"
macro: "#a6e22e"
preproc: "#f92672"
operator: "#f92672"
property: "#fda5ff"
label: "#f92672"
attribute: "#a6e22e"
bg: "#272822"
normal: "#f8f8f2"
}
readonly property Scheme gruvboxDark: Scheme {
comment: "#928374"
string: "#b8bb26"
stringKey: "#ebdbb2"
number: "#d3869b"
keyword: "#fb4934"
type: "#fabd2f"
functions: "#b8bb26"
method: "#b8bb26"
macro: "#8ec07c"
preproc: "#8ec07c"
operator: "#ebdbb2"
property: "#83a598"
label: "#fb4934"
attribute: "#8ec07c"
bg: "#1d2021"
normal: "#ebdbb2"
}
readonly property Scheme catppuccinMocha: Scheme {
comment: "#9399b2"
string: "#a6e3a1"
stringKey: "#b4befe"
number: "#fab387"
keyword: "#cba6f7"
type: "#f9e2af"
functions: "#89b4fa"
method: "#89b4fa"
macro: "#cba6f7"
preproc: "#f5c2e7"
operator: "#89dceb"
property: "#b4befe"
label: "#74c7ec"
attribute: "#f9e2af"
bg: "#1e1e2e"
normal: "#cdd6f4"
}
readonly property Scheme tokyoNight: Scheme {
comment: "#565f89"
string: "#9ece6a"
stringKey: "#73daca"
number: "#ff9e64"
keyword: "#9d7cd8"
type: "#2ac3de"
functions: "#7aa2f7"
method: "#7aa2f7"
macro: "#7dcfff"
preproc: "#7dcfff"
operator: "#89ddff"
property: "#73daca"
label: "#7aa2f7"
attribute: "#73daca"
bg: "#1a1b26"
normal: "#c0caf5"
}
readonly property Scheme ayuDark: Scheme {
comment: "#5a6673"
string: "#aad94c"
stringKey: "#aad94c"
number: "#d2a6ff"
keyword: "#ff8f40"
type: "#59c2ff"
functions: "#ffb454"
method: "#ffb454"
macro: "#59c2ff"
preproc: "#ff8f40"
operator: "#f29668"
property: "#f07178"
label: "#59c2ff"
attribute: "#ffb454"
bg: "#0a0e14"
normal: "#bfbdb6"
}
readonly property Scheme palenight: Scheme {
comment: "#697098"
string: "#c3e88d"
stringKey: "#82b1ff"
number: "#f78c6c"
keyword: "#ff5370"
type: "#ffcb6b"
functions: "#82b1ff"
method: "#82b1ff"
macro: "#c792ea"
preproc: "#ffcb6b"
operator: "#89ddff"
property: "#c3e88d"
label: "#c792ea"
attribute: "#ffcb6b"
bg: "#292d3e"
normal: "#bfc7d5"
}
}
readonly property var active: schemes[Config.llm.appearance.scheme]
component Scheme: JsonObject {
property color comment: "#697098"
property color string: "#c3e88d"
property color stringKey: "#82b1ff"
property color number: "#f78c6c"
property color keyword: "#ff5370"
property color type: "#ffcb6b"
property color functions: "#82b1ff"
property color method: "#82b1ff"
property color macro: "#c792ea"
property color preproc: "#ffcb6b"
property color operator: "#89ddff"
property color property: "#c3e88d"
property color label: "#c792ea"
property color attribute: "#ffcb6b"
property color bg: "#292d3e"
property color normal: "#bfc7d5"
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,166 @@
import QtQuick
import QtQuick.Layouts
import ZShell.Llm
import ZShell.Config
import qs.Modules.Notifications.Sidebar.Chat
import qs.Components
import qs.Services
Item {
id: root
required property bool isUser
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
readonly property real contentMaxWidth: width - Tokens.spacing.extraSmall - Tokens.spacing.extraLarge
signal edit(text: string)
implicitHeight: bubble.implicitHeight + (actionsRow.shouldBeActive ? actionsRow.implicitHeight + actionsRow.anchors.topMargin : 0)
CustomClippingRect {
id: bubble
property real liveTargetWidth: root.isUser ? Math.min(msgText.implicitWidth + Tokens.padding.medium * 2, root.contentMaxWidth) : root.contentMaxWidth
property real frozenWidth: 0.0
property bool layoutFrozen: false
property real layoutWidth: root.isUser ? Math.min(msgText.implicitWidth + Tokens.padding.medium * 2, root.contentMaxWidth) : root.contentMaxWidth
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 : markdown.implicitHeight + markdown.anchors.topMargin * 2
anchors.right: root.isUser ? parent.right : undefined
layer.enabled: false
layer.smooth: true
transform: Scale {
id: stretchScale
origin.x: root.isUser ? bubble.width : 0
xScale: bubble.layoutFrozen && bubble.frozenWidth > 0 ? bubble.liveTargetWidth / bubble.frozenWidth : 1.0
}
states: [
State {
name: "toSlim"
when: ChatState.isWindow && !ChatState.isWide && !ChatState.settledSlim
PropertyChanges {
bubble.layer.enabled: true
bubble.layoutFrozen: true
}
},
State {
name: "toWide"
when: ChatState.isWindow && ChatState.isWide && !ChatState.settledWide
PropertyChanges {
bubble.layer.enabled: true
bubble.layoutFrozen: true
}
}
]
onLayoutFrozenChanged: {
if (layoutFrozen)
frozenWidth = width;
}
TextEditBase {
id: msgText
property string cachedText: root.text
property bool cancelled: false
visible: root.isUser
anchors.left: parent.left
anchors.margins: Tokens.padding.medium
anchors.right: parent.right
anchors.top: parent.top
animateCursor: false
color: root.isUser ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
cursor.color: root.isUser ? Colors.palette.m3onPrimary : Colors.palette.m3primary
font: Tokens.font.body.small
readOnly: true
selectionColor: Qt.alpha((root.isUser ? Colors.palette.m3onPrimary : Colors.palette.m3primary), 0.4)
text: root.isUser ? root.text : ""
textFormat: CustomText.MarkdownText
wrapMode: Text.WordWrap
Keys.onPressed: event => {
if (event.key == Qt.Key_Return) {
if (!(event.modifiers & Qt.ShiftModifier)) {
readOnly = true;
event.accepted = true;
}
} else if (event.key == Qt.Key_Escape) {
cancelled = true;
readOnly = true;
event.accepted = true;
}
}
onEditingFinished: {
readOnly = true;
}
onReadOnlyChanged: {
if (readOnly) {
animateCursor = false;
textFormat = CustomText.MarkdownText;
if (cancelled) {
text = cachedText;
cancelled = false;
return;
}
root.edit(text);
} else {
textFormat = CustomText.PlainText;
text = cachedText;
forceActiveFocus();
cursorPosition = text.length;
animateCursor = true;
}
}
}
MarkdownBlocks {
id: markdown
visible: !root.isUser
anchors.topMargin: Tokens.padding.medium
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
blocks: root.blocks
}
}
Actions {
id: actionsRow
readonly property bool shouldBeActive: !root.running && root.repeater.count === (root.index + 1)
anchors.left: parent.left
anchors.right: (implicitWidth > bubble.implicitWidth) ? undefined : bubble.right
anchors.top: bubble.bottom
opacity: shouldBeActive ? 1 : 0
visible: opacity > 0
anchors.topMargin: Tokens.spacing.medium
edit: msgText
hovered: root.hovered
isUser: root.isUser
running: root.running
message: root.message
Behavior on opacity {
Anim {}
}
}
}
@@ -0,0 +1,32 @@
import QtQuick
import QtQuick.Layouts
import ZShell.Config
import qs.Components
import qs.Services
ColumnLayout {
id: root
spacing: Tokens.spacing.small
Item {
Layout.fillHeight: true
}
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
color: Colors.tPalette.m3outlineVariant
fontStyle: Tokens.font.icon.extraLarge
text: "chat"
}
CustomText {
Layout.alignment: Qt.AlignHCenter
color: Colors.tPalette.m3onSurfaceVariant
text: qsTr("Send a message to get started")
}
Item {
Layout.fillHeight: true
}
}
@@ -0,0 +1,128 @@
import QtQuick
import QtQuick.Layouts
import ZShell.Llm
import ZShell.Config
import qs.Components
import qs.Services
Item {
id: root
required property ChatGeneration current
signal dismiss
implicitHeight: 0
Component.onCompleted: {
root.implicitHeight = Qt.binding(() => errorInner.implicitHeight + errorInner.anchors.margins * 2);
enterAnim.start();
}
Behavior on implicitHeight {
Anim {}
}
ParallelAnimation {
id: enterAnim
Anim {
target: container
property: "y"
from: container.implicitHeight
to: 0
}
Anim {
target: container
property: "opacity"
from: 0
to: 1
}
}
ParallelAnimation {
id: exitAnim
Anim {
target: root
property: "implicitHeight"
from: root.implicitHeight
to: 0
}
Anim {
target: container
property: "y"
to: container.implicitHeight
from: 0
}
Anim {
target: container
property: "opacity"
to: 0
from: 1
}
onFinished: root.dismiss()
}
CustomRect {
id: container
anchors.left: parent.left
anchors.right: parent.right
radius: Tokens.rounding.medium
color: Colors.palette.m3errorContainer
implicitHeight: errorInner.implicitHeight + errorInner.anchors.margins * 2
ColumnLayout {
id: errorInner
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Tokens.padding.medium
spacing: Tokens.spacing.small
CustomText {
id: header
Layout.fillWidth: true
font: Tokens.font.headline.small
elide: CustomText.ElideRight
color: Colors.palette.m3onErrorContainer
text: qsTr("An error occured")
}
CustomText {
id: body
Layout.fillWidth: true
wrapMode: CustomText.WordWrap
color: Colors.palette.m3onErrorContainer
text: root.current.errorMessage || LlmClient.serverError
}
}
IconButton {
anchors.top: parent.top
anchors.right: parent.right
anchors.margins: Tokens.padding.medium
isRound: true
icon: "close"
type: IconButton.Text
font: Tokens.font.icon.medium
onClicked: exitAnim.start()
}
}
}
@@ -0,0 +1,90 @@
import QtQuick
import Quickshell
import ZShell.Config
import ZShell.Llm
import qs.Components
import qs.Services
Column {
id: root
required property var blocks
spacing: Tokens.spacing.small
Repeater {
id: blockRep
model: ScriptModel {
values: root.blocks
objectProp: "id"
}
delegate: DelegateChooser {
role: "type"
DelegateChoice {
roleValue: MarkdownBlock.Type.Code
delegate: CodeBlockView {
required property MarkdownBlock modelData
language: modelData.language
code: modelData.code
anchors.right: parent.right
anchors.margins: Tokens.spacing.small
anchors.left: parent.left
}
}
DelegateChoice {
roleValue: MarkdownBlock.Type.Math
delegate: MathBlockView {
required property MarkdownBlock modelData
latex: modelData.latex
anchors.right: parent.right
anchors.left: parent.left
anchors.margins: Tokens.padding.medium
}
}
DelegateChoice {
roleValue: MarkdownBlock.Type.Heading
delegate: TextEditBase {
required property MarkdownBlock modelData
color: Colors.palette.m3onSurface
anchors.right: parent.right
anchors.left: parent.left
anchors.margins: Tokens.padding.medium
text: modelData.text
textFormat: Text.MarkdownText
readOnly: true
font: {
if (modelData.level <= 1)
return Tokens.font.title.builders.large.weight(Font.Bold).build();
if (modelData.level === 2)
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: MarkdownBlock.Type.Text
delegate: BubbleEdit {
required property MarkdownBlock modelData
anchors.right: parent.right
anchors.left: parent.left
anchors.margins: Tokens.padding.medium
text: modelData.text
}
}
}
}
}
@@ -0,0 +1,47 @@
import QtQuick
import ZShell.Config
import ZShell.Llm
import qs.Components
import qs.Services
Item {
id: root
required property string latex
property color mathColor: Colors.palette.m3onSurface
property font font: Tokens.font.headline.medium
implicitHeight: Math.max(equationImage.height, fallbackText.contentHeight) + Tokens.padding.small * 2
LlmMathText {
id: math
latex: root.latex
color: root.mathColor
fontPointSize: root.font.pointSize
devicePixelRatio: Screen.devicePixelRatio
}
Image {
id: equationImage
anchors.centerIn: parent
source: math.imageId.length ? "image://llmmath/" + math.imageId : ""
visible: math.ok
fillMode: Image.PreserveAspectFit
smooth: true
width: Math.min(math.width, root.width)
height: math.width > 0 ? width * (math.height / math.width) : 0
}
CustomText {
id: fallbackText
anchors.centerIn: parent
color: root.mathColor
font: Tokens.font.mono.medium
text: root.latex
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
visible: !math.ok
}
}
@@ -0,0 +1,363 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import ZShell.Components
import ZShell.Config
import ZShell.Llm
import qs.Modules.Notifications.Sidebar.Chat
import qs.Components
import qs.Services
MouseArea {
id: root
required property int index
required property Item rootParent
required property ChatMessage modelData
required property ChatListView chatList
readonly property bool isUser: modelData.role === ChatMessage.Role.User
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
property real savedOffset: NaN
property bool anchored: false
function handleReasoningToggle(expanded: bool, block: Item): void {
if (!root.rootParent)
return;
if (expanded) {
root.savedOffset = root.rootParent.contentY - root.y;
root.anchored = true;
chatList.anchorItem(block, ChatListView.TopEdge);
settleTimer.start();
} else {
settleTimer.stop();
root.anchored = false;
chatList.clearAnchor();
if (!isNaN(root.savedOffset)) {
chatList.requestRestore(root.y + root.savedOffset);
root.savedOffset = NaN;
}
}
}
function blockify(segs, errored) {
const blocks = [];
for (let i = 0; i < segs.length; i++) {
const seg = segs[i];
if (seg.type === LlmSegment.Type.Content) {
blocks.push({
kind: "content",
id: i,
text: seg.text,
markdown: seg.markdown,
running: seg.running
});
} else {
const last = blocks.length ? blocks[blocks.length - 1] : null;
if (last && last.kind === "process") {
last.segments.push(seg);
} else {
blocks.push({
kind: "process",
id: i,
segments: [seg]
});
}
}
}
if (errored)
blocks.push({
kind: "error",
id: segs.length
});
return blocks;
}
hoverEnabled: true
preventStealing: false
implicitHeight: layout.implicitHeight + Tokens.padding.medium + (attachments.visible ? attachments.implicitHeight : 0)
onImplicitHeightChanged: if (root.anchored)
settleTimer.restart()
Timer {
id: settleTimer
interval: Tokens.anim.durations.expressiveDefaultEffects
repeat: false
onTriggered: {
if (root.anchored) {
root.anchored = false;
chatList.clearAnchor();
}
}
}
Behavior on current {
id: changeAnim
property var object: targetProperty
readonly property bool next: {
const cond = root.modelData.generations.indexOf(current) < root.modelData.generations.indexOf(changeAnim.targetValue);
return cond;
}
animation: SequentialAnimation {
ParallelAnimation {
Anim {
target: root
property: "x"
to: changeAnim.next ? root.width / 4 : -root.width / 4
from: 0
type: Anim.FastEffects
}
Anim {
target: root
property: "opacity"
from: 1
to: 0
type: Anim.FastEffects
}
}
PropertyAction {}
ParallelAnimation {
Anim {
target: root
property: "x"
from: changeAnim.next ? -root.width / 4 : root.width / 4
type: Anim.FastEffects
to: 0
}
Anim {
target: root
property: "opacity"
from: 0
type: Anim.FastEffects
to: 1
}
}
}
}
GridView {
id: attachments
readonly property int columns: ChatState.isWindow ? 5 : 3
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
flow: GridView.LeftToRight
verticalLayoutDirection: GridView.BottomToTop
cellWidth: width / columns
cellHeight: cellWidth
implicitHeight: Math.ceil(count / columns) * cellHeight
displaced: Transition {
Anim {
property: "x"
}
Anim {
property: "y"
}
}
model: ScriptModel {
objectProp: "path"
values: {
const list = [...root.modelData.attachments];
if (list.length > 0 && list.length < attachments.columns) {
while (list.length < attachments.columns)
list.unshift(null);
}
return list;
}
}
delegate: Item {
id: thumb
required property var modelData
function fileIcon(mime): string {
if (mime.startsWith("archive/") || mime === "application/zip")
return "folder_zip";
if (mime === "application/pdf")
return "picture_as_pdf";
if (mime.startsWith("audio/"))
return "audio_file";
if (mime.startsWith("video/"))
return "video_file";
if (mime.startsWith("text/"))
return "description";
return "insert_drive_file";
}
width: attachments.cellWidth
height: width
opacity: modelData ? 1 : 0
CustomClippingRect {
anchors.fill: parent
anchors.margins: Tokens.spacing.extraSmall
radius: Tokens.rounding.medium
color: Colors.tPalette.m3surfaceContainer
FadeImage {
id: attImage
anchors.fill: parent
fillMode: FadeImage.PreserveAspectCrop
asynchronous: true
source: visible ? thumb.modelData?.path ?? "" : ""
visible: thumb.modelData?.isImage ?? false
}
MaterialIcon {
anchors.centerIn: parent
fontStyle: Tokens.font.icon.extraLarge
text: thumb.fileIcon(thumb.modelData?.mimeType ?? "")
visible: !thumb.modelData?.isImage ?? false
}
Loader {
id: attIndicatorLoader
anchors.centerIn: parent
opacity: 0
active: opacity > 0 && (thumb.modelData?.isImage ?? false)
sourceComponent: CustomRect {
implicitWidth: attLoadingIndicator.implicitSize + Tokens.padding.largeIncreased * 2
implicitHeight: attLoadingIndicator.implicitSize + Tokens.padding.largeIncreased * 2
color: Colors.palette.m3primaryContainer
radius: Tokens.rounding.full
LoadingIndicator {
id: attLoadingIndicator
anchors.centerIn: parent
containsIcon: true
implicitSize: thumb.implicitWidth * 0.4
}
}
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
}
Timer {
id: attLoadDebounceTimer
interval: 100
onTriggered: {
if (attImage.status !== Image.Ready)
attIndicatorLoader.opacity = 1;
}
}
}
}
}
Column {
id: layout
anchors.top: attachments.bottom
anchors.left: parent.left
anchors.right: parent.right
spacing: Tokens.spacing.medium
Repeater {
id: segmentRep
model: ScriptModel {
values: root.blocks
objectProp: "id"
}
delegate: DelegateChooser {
role: "kind"
DelegateChoice {
roleValue: "error"
delegate: ErrorBubble {
width: root.width
current: root.current
onDismiss: root.current.dismissError()
}
}
DelegateChoice {
roleValue: "process"
delegate: ProcessBlock {
width: root.width
blocks: root.blocks
current: root.current
rootParent: root.rootParent
onExpandedChanged: root.handleReasoningToggle(expanded, this)
}
}
DelegateChoice {
roleValue: "content"
delegate: ContentBubble {
required property var modelData
isUser: root.isUser
text: modelData.text
blocks: modelData.markdown
running: modelData.running
width: root.width
repeater: segmentRep
message: root.modelData
hovered: root.containsMouse
onEdit: text => {
root.modelData.edit(text);
if (root.isUser)
root.modelData.generate();
}
}
}
}
}
}
}
@@ -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;
}
}
}
}
}
}
}
}
@@ -0,0 +1,240 @@
import QtQuick
import QtQuick.Layouts
import ZShell.Blobs
import ZShell.Config
import ZShell.Components
import ZShell.Llm
import qs.Components
import qs.Services
Item {
id: root
required property int index
required property var modelData
required property var blocks
readonly property var segments: modelData.segments
required property Item rootParent
required property ChatGeneration current
readonly property bool isActive: index === blocks.length - 1
property bool expanded: false
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.some(s => s.type === LlmSegment.Type.ToolCall && s.status === LlmToolCall.Status.Pending)
implicitHeight: {
const expand = expandedRect.implicitHeight + layout.implicitHeight + expandedRect.anchors.topMargin;
const collapsed = layout.implicitHeight;
const approval = layout.implicitHeight + approvalPrompt.implicitHeight + approvalPrompt.anchors.topMargin;
if (expanded)
return expand;
else if (pendingApproval)
return approval;
else
return collapsed;
}
onCurrentChanged: expanded = false
Behavior on implicitHeight {
Anim {
type: Anim.DefaultEffects
}
}
RowLayout {
id: layout
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
Item {
implicitHeight: expandBtn.implicitHeight
implicitWidth: expandBtn.implicitWidth
LoadingIndicator {
id: spinnerReasoning
implicitSize: collapsedText.implicitHeight
anchors.centerIn: parent
opacity: root.isActive ? 1 : 0
visible: opacity > 0
Behavior on opacity {
Anim {}
}
}
IconButton {
id: expandBtn
font: Tokens.font.icon.medium
icon: "keyboard_arrow_down"
inactiveOnColor: hovered ? Colors.palette.m3onSurface : Colors.palette.m3outline
anchors.centerIn: parent
opacity: root.isActive ? 0 : 1
enabled: opacity > 0
rotation: root.expanded ? 180 : 0
type: IconButton.Text
Behavior on rotation {
Anim {}
}
onClicked: root.expanded = !root.expanded
}
}
CustomText {
id: collapsedText
color: Colors.palette.m3outline
font: Tokens.font.label.small
Layout.fillWidth: true
Layout.leftMargin: Tokens.spacing.medium
text: {
if (root.isActive) {
if (root.pendingApproval)
return qsTr("Waiting for approval...");
if (root.lastSegment.type === LlmSegment.Type.ToolCall)
return qsTr("Using %1...").arg(root.lastSegment.name);
return qsTr("Thinking...");
}
return qsTr("Worked for %1s").arg((root.totalElapsedMs / 1000).toFixed(1));
}
Behavior on opacity {
Anim {}
}
}
}
ApprovalPrompt {
id: approvalPrompt
anchors.top: layout.bottom
anchors.topMargin: Tokens.spacing.small
anchors.left: parent.left
anchors.right: parent.right
visible: root.pendingApproval
rootParent: root.rootParent
current: root.current
}
CustomClippingRect {
id: expandedRect
anchors.left: parent.left
anchors.right: parent.right
anchors.top: layout.bottom
anchors.bottom: parent.bottom
anchors.topMargin: Tokens.spacing.small
color: Colors.palette.m3surfaceContainerLow
implicitHeight: expandedContent.contentHeight + expandedContent.anchors.margins * 2
opacity: root.expanded ? 1 : 0
radius: Tokens.rounding.medium
visible: opacity > 0
Behavior on opacity {
Anim {}
}
ListView {
id: expandedContent
anchors.fill: parent
anchors.margins: Tokens.padding.medium
model: root.segments
spacing: Tokens.spacing.medium
delegate: Item {
id: segment
required property var modelData
required property int index
implicitWidth: ListView.view.width
implicitHeight: header.implicitHeight + content.implicitHeight + content.anchors.topMargin
RowLayout {
id: header
anchors.left: parent.left
anchors.right: parent.right
spacing: Tokens.spacing.small
MaterialIcon {
text: segment.modelData.type === LlmSegment.Type.Reasoning ? "cognition" : "build"
fill: 1
}
CustomText {
Layout.fillWidth: true
text: segment.modelData.type === LlmSegment.Type.Reasoning ? qsTr("Thought for %1s").arg((segment.modelData.elapsedMs / 1000).toFixed(1)) : qsTr("Used %1 for %2s").arg(segment.modelData.name).arg((segment.modelData.elapsedMs / 1000).toFixed(1))
}
}
CustomText {
id: content
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);
}
function prettyTool(seg: var): string {
if (seg.name === "webfetch") {
switch (seg.status) {
case LlmToolCall.Status.Running:
return qsTr("Fetching");
case LlmToolCall.Status.Pending:
return qsTr("Pending fetch");
case LlmToolCall.Status.Success:
return qsTr("Fetched");
case LlmToolCall.Status.Error:
return qsTr("Failed to fetch");
default:
return qsTr("Fetched website");
}
} else if (seg.name === "readfile") {
switch (seg.status) {
case LlmToolCall.Status.Running:
return qsTr("Reading");
case LlmToolCall.Status.Pending:
return qsTr("Pending read");
case LlmToolCall.Status.Success:
return qsTr("Read");
case LlmToolCall.Status.Error:
return qsTr("Failed to read");
default:
return qsTr("Read file");
}
}
}
anchors.left: parent.left
anchors.leftMargin: Tokens.padding.small
anchors.top: header.bottom
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))
}
}
}
}
}
@@ -0,0 +1,53 @@
pragma Singleton
import Quickshell
import QtQuick
import ZShell.Config
import qs.Helpers
import qs.Services
Singleton {
id: root
function create(parent: Item, props: var): void {
chatComp.createObject(parent ?? dummy, props);
ShellState.forActive().sidebar = false;
ChatState.isWindow = true;
}
QtObject {
id: dummy
}
Component {
id: chatComp
FloatingWindow {
id: win
property var props
color: Colors.tPalette.m3surface
implicitHeight: chat.implicitHeight
implicitWidth: chat.implicitWidth
minimumSize.height: Config.sidebar.sizes.width
minimumSize.width: Config.sidebar.sizes.width
surfaceFormat.opaque: false
title: qsTr("ZShell - %1").arg(ChatState.title)
Component.onCompleted: ChatState.screen = screen
onVisibleChanged: {
if (!visible) {
destroy();
ChatState.isWindow = false;
}
}
SidebarView {
id: chat
anchors.fill: parent
}
}
}
}
@@ -0,0 +1,207 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import ZShell.Blobs
import ZShell.Config
import ZShell.Llm
import qs.Components
import qs.Modules.Notifications.Sidebar.Chat.Content
import qs.Services
Item {
id: root
property int breakpoint: Math.round(Config.sidebar.sizes.width * 2) + Tokens.spacing.medium * 2 + Tokens.padding.medium * 3
property color blobColor: Colors.tPalette.m3surfaceContainerLow
readonly property int iconSize: Math.round(Tokens.font.icon.extraLarge.pointSize * (96 / 72)) + Tokens.padding.large * 2
property alias conversationModel: sidebar.model
property bool chatOpen: ChatState.chatSession !== null
readonly property bool isWide: root.width >= root.breakpoint
readonly property bool showList: isWide || !chatOpen
readonly property bool showContent: !isWide && chatOpen
function openConversation(conv: ChatSession, index: int): void {
}
BlobGroup {
id: blobGroup
color: root.blobColor
smoothing: Tokens.rounding.medium
}
BlobInvertedRect {
id: invertedRect
anchors.fill: parent
borderBottom: Tokens.padding.small
borderLeft: sidebar.implicitWidth + sidebar.anchors.margins + Tokens.spacing.medium
borderRight: Tokens.padding.small
borderTop: Tokens.padding.small
group: blobGroup
opacity: root.blobColor.a
radius: Tokens.rounding.largeIncreased
}
ChatList {
id: sidebar
anchors.top: parent.top
anchors.left: parent.left
anchors.bottom: parent.bottom
anchors.margins: Tokens.padding.medium
implicitWidth: root.isWide || ChatState.narrowSidebarExpanded ? Config.sidebar.sizes.width : root.iconSize
highlight: true
slim: !root.isWide && !ChatState.narrowSidebarExpanded
z: 1
Behavior on implicitWidth {
Anim {
type: Anim.DefaultEffects
}
}
model: LlmClient.chats
onLoadChatRequest: (chat, index) => {
ChatState.currentIdx = index;
ChatState.chatSession = chat;
}
onDeleteChatRequest: chat => {
const wasCurrent = chat === ChatState.chatSession;
LlmClient.chats.remove(chat);
if (wasCurrent)
ChatState.openDraft();
}
onNewChatRequest: ChatState.openDraft()
onRequestExpand: {
ChatState.narrowSidebarExpanded = true;
}
}
CustomClippingRect {
anchors.fill: parent
anchors.leftMargin: invertedRect.borderLeft
anchors.topMargin: invertedRect.borderTop
anchors.bottomMargin: invertedRect.borderBottom
anchors.rightMargin: invertedRect.borderRight
radius: invertedRect.radius
children: [contentArea, dim]
}
CustomRect {
id: dim
anchors.fill: parent
color: Colors.palette.m3shadow
opacity: ChatState.narrowSidebarExpanded && !root.isWide ? 0.3 : 0
z: 1
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
}
Item {
id: contentArea
property int leftMargin: sidebar.implicitWidth
z: 0
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: parent.right
implicitWidth: root.width - leftMargin - sidebar.anchors.margins - Tokens.spacing.medium * 2
states: State {
name: "wide"
when: !root.isWide
PropertyChanges {
contentArea.leftMargin: root.iconSize
}
}
transitions: Transition {
to: "wide"
Anim {
type: Anim.DefaultEffects
property: "leftMargin"
}
}
ChatHost {
id: convHost
anchors.fill: parent
clip: true
onLoadingChanged: if (!loading)
root.chatOpen = true
}
MouseArea {
id: dimArea
anchors.fill: parent
preventStealing: true
enabled: !root.isWide && ChatState.narrowSidebarExpanded
cursorShape: undefined
onClicked: {
const x = mouseX;
const margin = root.width - contentArea.implicitWidth - contentArea.anchors.margins * 2;
if (x > invertedRect.borderLeft - margin) {
ChatState.narrowSidebarExpanded = false;
ChatState.fabExpanded = false;
}
}
HoverHandler {
cursorShape: dimArea.enabled ? Qt.ArrowCursor : undefined
enabled: dimArea.enabled
}
}
}
Binding {
target: ChatState
property: "windowIsWide"
value: root.showList
}
Binding {
target: ChatState
property: "isWide"
value: root.isWide
}
Binding {
target: ChatState
value: contentArea.leftMargin === root.iconSize
property: "settledSlim"
}
Binding {
target: ChatState
value: contentArea.leftMargin === sidebar.implicitWidth
property: "settledWide"
}
Component {
id: conversationView
ChatContent {}
}
}
+140 -6
View File
@@ -1,7 +1,11 @@
import qs.Components pragma ComponentBehavior: Bound
import ZShell.Config import ZShell.Config
import ZShell.Llm
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import qs.Components
import qs.Modules.Notifications.Sidebar.Chat
import qs.Services import qs.Services
Item { Item {
@@ -9,20 +13,150 @@ Item {
required property ScreenState screenState required property ScreenState screenState
Connections {
function onIsWindowChanged(): void {
if (ChatState.isWindow)
root.screenState.sidebarTab = 0;
}
target: ChatState
}
ColumnLayout { ColumnLayout {
id: layout id: layout
anchors.fill: parent anchors.fill: parent
spacing: Tokens.spacing.small spacing: Tokens.spacing.small
CustomRect { Tabs {
Layout.fillWidth: true
Layout.preferredHeight: ChatState.isWindow || !Config.llm.enabled ? 0 : implicitHeight
screenState: root.screenState
nonAnimWidth: layout.width
visible: height > 0
}
Item {
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
color: Colors.tPalette.m3surfaceContainerLow clip: true
radius: Tokens.rounding.small
NotifDock { Item {
screenState: root.screenState id: pages
anchors.bottom: parent.bottom
anchors.top: parent.top
implicitWidth: parent.width
opacity: root.screenState.sidebarTab === 0 ? 1 : 0
visible: opacity > 0
x: root.screenState.sidebarTab === 0 ? 0 : -root.width
Behavior on opacity {
Anim {}
}
Behavior on x {
Anim {}
}
CustomRect {
id: bg
anchors.fill: parent
color: Colors.tPalette.m3surfaceContainerLow
radius: Tokens.rounding.small
NotifDock {
screenState: root.screenState
}
}
}
Loader {
id: chatLoader
anchors.bottom: parent.bottom
anchors.top: parent.top
active: Config.llm.enabled
width: parent.width
property bool tre: true
sourceComponent: Item {
id: chatPage
opacity: root.screenState.sidebarTab === 1 ? 1 : 0
visible: opacity > 0
x: root.screenState.sidebarTab === 0 ? root.width : 0
z: 1
Behavior on opacity {
Anim {}
}
Behavior on x {
Anim {}
}
CustomRect {
anchors.fill: parent
color: Colors.tPalette.m3surfaceContainerLow
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
}
}
}
}
}
} }
} }
+139
View File
@@ -0,0 +1,139 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import QtQuick.Templates
import Quickshell
import ZShell.Config
import qs.Components
import qs.Services
Item {
id: root
readonly property alias count: bar.count
required property ScreenState screenState
required property real nonAnimWidth
implicitHeight: bar.implicitHeight + indicator.implicitHeight + indicator.anchors.topMargin + separator.implicitHeight
TabBar {
id: bar
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
background: null
currentIndex: root.screenState.sidebarTab
implicitHeight: contentHeight
contentItem: RowLayout {
spacing: 0
Repeater {
model: bar.contentModel
}
}
onCurrentIndexChanged: root.state.currentTab = currentIndex
Tab {
iconName: "notifications"
text: qsTr("Notifications")
}
Tab {
iconName: "chat"
text: qsTr("Chat")
}
}
Item {
id: indicator
anchors.top: bar.bottom
clip: true
implicitHeight: 3
implicitWidth: bar.currentItem.implicitWidth
x: {
const tab = bar.currentItem;
const width = (root.nonAnimWidth - bar.spacing * (bar.count - 1)) / bar.count;
return width * tab.TabBar.index + (width - tab.implicitWidth) / 2;
}
Behavior on implicitWidth {
Anim {}
}
Behavior on x {
Anim {}
}
CustomRect {
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
color: Colors.palette.m3primary
implicitHeight: parent.implicitHeight * 2
radius: Tokens.rounding.full
}
}
CustomRect {
id: separator
anchors.left: parent.left
anchors.right: parent.right
anchors.top: indicator.bottom
color: Colors.palette.m3outlineVariant
implicitHeight: 1
}
component Tab: TabButton {
id: tab
readonly property bool current: TabBar.tabBar.currentItem === this
required property string iconName
Layout.fillWidth: true
Layout.preferredWidth: 1
background: null
implicitHeight: implicitContentHeight
implicitWidth: implicitContentWidth
contentItem: Item {
implicitHeight: icon.height + label.height
implicitWidth: Math.max(icon.width, label.width)
StateLayer {
color: tab.current ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
radius: Tokens.rounding.small
onClicked: root.screenState.sidebarTab = tab.TabBar.index
}
MaterialIcon {
id: icon
anchors.bottom: label.top
anchors.horizontalCenter: parent.horizontalCenter
color: tab.current ? Colors.palette.m3primary : Colors.palette.m3onSurfaceVariant
fill: tab.current ? 1 : 0
fontStyle: Tokens.font.icon.large
text: tab.iconName
Behavior on fill {
Anim {}
}
}
CustomText {
id: label
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
color: tab.current ? Colors.palette.m3primary : Colors.palette.m3onSurfaceVariant
text: tab.text
}
}
}
}
@@ -140,8 +140,7 @@ ColumnLayout {
} }
} }
Behavior on implicitHeight { Behavior on implicitHeight {
Anim { Anim {}
}
} }
model: FileSystemModel { model: FileSystemModel {
nameFilters: ["recording_*.mp4"] nameFilters: ["recording_*.mp4"]
@@ -167,8 +166,7 @@ ColumnLayout {
opacity: list.count === 0 ? 1 : 0 opacity: list.count === 0 ? 1 : 0
Behavior on opacity { Behavior on opacity {
Anim { Anim {}
}
} }
sourceComponent: ColumnLayout { sourceComponent: ColumnLayout {
spacing: Tokens.spacing.small spacing: Tokens.spacing.small
@@ -183,16 +181,13 @@ ColumnLayout {
text: "scan_delete" text: "scan_delete"
Behavior on Layout.preferredHeight { Behavior on Layout.preferredHeight {
Anim { Anim {}
}
} }
Behavior on opacity { Behavior on opacity {
Anim { Anim {}
}
} }
Behavior on scale { Behavior on scale {
Anim { Anim {}
}
} }
} }
@@ -208,16 +203,13 @@ ColumnLayout {
text: "scan_delete" text: "scan_delete"
Behavior on Layout.preferredWidth { Behavior on Layout.preferredWidth {
Anim { Anim {}
}
} }
Behavior on opacity { Behavior on opacity {
Anim { Anim {}
}
} }
Behavior on scale { Behavior on scale {
Anim { Anim {}
}
} }
} }
@@ -1,9 +1,9 @@
import Quickshell import Quickshell
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import ZShell.Config
import qs.Components import qs.Components
import qs.Modules.Notifications.Sidebar.Utils.Cards import qs.Modules.Notifications.Sidebar.Utils.Cards
import ZShell.Config
Item { Item {
id: root id: root
@@ -19,8 +19,7 @@ Item {
anchors.fill: parent anchors.fill: parent
spacing: 8 spacing: 8
IdleInhibit { IdleInhibit {}
}
Record { Record {
screenState: root.screenState screenState: root.screenState
@@ -4,7 +4,6 @@ import QtQuick
import Quickshell import Quickshell
import ZShell.Config import ZShell.Config
import qs.Components import qs.Components
import qs.Modules.Bar.Popouts as BarPopouts
Item { Item {
id: root id: root
@@ -15,7 +14,7 @@ Item {
required property Item sidebar required property Item sidebar
anchors.bottomMargin: (-implicitHeight - 5) * offsetScale anchors.bottomMargin: (-implicitHeight - 5) * offsetScale
implicitHeight: content.implicitHeight + 8 * 2 implicitHeight: content.implicitHeight + Tokens.padding.small * 2
implicitWidth: sidebar.width * (1 - sidebar.offsetScale) implicitWidth: sidebar.width * (1 - sidebar.offsetScale)
opacity: 1 - offsetScale opacity: 1 - offsetScale
visible: offsetScale < 1 visible: offsetScale < 1
+15
View File
@@ -1,7 +1,11 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import qs.Components import qs.Components
import qs.Components.Toast
import ZShell
import ZShell.Config import ZShell.Config
import ZShell.Llm
import Quickshell
import QtQuick import QtQuick
Item { Item {
@@ -25,6 +29,17 @@ Item {
} }
} }
Connections {
target: LlmClient
function onGenerationFailed(session: ChatSession, message: string): void {
if (root.shouldBeActive && root.chatActive)
return;
Toaster.toast(qsTr("Chat"), message, "error_outline", Toast.Error);
}
}
Loader { Loader {
id: content id: content
+2 -1
View File
@@ -51,7 +51,8 @@ CustomRect {
value: Memory.percentage value: Memory.percentage
Behavior on clampedVal { Behavior on clampedVal {
Anim {} Anim {
}
} }
ColumnLayout { ColumnLayout {
+1 -1
View File
@@ -121,6 +121,6 @@ CustomRect {
activeIcon: "storage" activeIcon: "storage"
icon: modelData === Storage.primaryDisk ? "check" : "" 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 bottomLeftRadius: last ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
bottomRightRadius: last ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall bottomRightRadius: last ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
color: Colors.tPalette.m3surfaceContainer 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 topLeftRadius: first ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
topRightRadius: first ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall topRightRadius: first ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
+16 -13
View File
@@ -10,24 +10,25 @@ import qs.Services
Item { Item {
id: root id: root
property bool acceptAllowed: true
required property string acceptLabel required property string acceptLabel
required property Component content required property Component content
property bool first: false
required property string header 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 int horizontalContentMargin
property string icon property string icon
property alias iconLabel: openButton.iconLabel property alias iconLabel: openButton.iconLabel
required property string label property string subtext: ""
property bool last: false property bool last: false
property bool open property bool open
property real openHeight: Math.min(rootParent.height * 0.8, 600) property real openHeight: Math.min(rootParent.height * 0.8, 600)
property real openWidth: Math.min(rootParent.width * 0.8, 400) property real openWidth: Math.min(rootParent.width * 0.8, 400)
required property Item rootParent
property alias rowButton: openButton property alias rowButton: openButton
property bool separateContent property bool separateContent
required property string settingAnchor
property string subtext: ""
signal accepted signal accepted
signal cancelled signal cancelled
@@ -50,8 +51,7 @@ Item {
color: root.open ? Colors.palette.m3surfaceContainerHighest : Colors.tPalette.m3surfaceContainer color: root.open ? Colors.palette.m3surfaceContainerHighest : Colors.tPalette.m3surfaceContainer
Behavior on color { Behavior on color {
CAnim { CAnim {}
}
} }
} }
@@ -139,12 +139,13 @@ Item {
id: dialogBg id: dialogBg
anchors.fill: parent anchors.fill: parent
bottomLeftRadius: Tokens.rounding.largeIncreased bottomLeftRadius: root.last ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
bottomRightRadius: Tokens.rounding.largeIncreased bottomRightRadius: root.last ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
deformScale: 0 deformScale: 0
group: blobGroup group: blobGroup
opacity: blobGroup.color.a * (root.enabled ? 1 : 0.5) opacity: blobGroup.color.a * (root.enabled ? 1 : 0.5)
radius: Tokens.rounding.extraSmall topLeftRadius: root.first ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
topRightRadius: root.first ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
} }
RowButton { RowButton {
@@ -153,9 +154,11 @@ Item {
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
color: "transparent" color: "transparent"
height: Math.min(implicitHeight, parent.height) // Clamp to parent height due to overshoot anim first: root.first
height: Math.min(implicitHeight, parent.height)
icon: root.icon icon: root.icon
last: true last: root.last
subtext: root.subtext
text: root.label text: root.label
transform: Matrix4x4 { transform: Matrix4x4 {
@@ -8,8 +8,9 @@ import qs.Services
DialogRowButton { DialogRowButton {
id: root id: root
property var initialSelect
required property var model required property var model
property var initialSelect
property var selectedItem property var selectedItem
function keyFor(item: var): string { function keyFor(item: var): string {
+16 -7
View File
@@ -30,7 +30,7 @@ ColumnLayout {
function findAnchor(item: Item, anchor: string): Item { function findAnchor(item: Item, anchor: string): Item {
if (!item) if (!item)
return null; return null;
if (item.settingAnchor !== undefined && item.settingAnchor === anchor) if (item.settingAnchor !== undefined && item.settingAnchor === anchor) // qmllint disable missing-property
return item; return item;
const kids = item.children; const kids = item.children;
for (let i = 0; i < kids.length; i++) { for (let i = 0; i < kids.length; i++) {
@@ -43,8 +43,8 @@ ColumnLayout {
function highlightAnchor(anchor: string): void { function highlightAnchor(anchor: string): void {
const row = findAnchor(contentChild, anchor); const row = findAnchor(contentChild, anchor);
if (row && row.flashHighlight !== undefined) if (row && row.flashHighlight !== undefined) // qmllint disable missing-property
row.flashHighlight(); row.flashHighlight(); // qmllint disable missing-property
} }
function scrollToAnchor(anchor: string): bool { function scrollToAnchor(anchor: string): bool {
@@ -61,8 +61,8 @@ ColumnLayout {
root.animateScroll = true; root.animateScroll = true;
flickable.contentY = target; flickable.contentY = target;
Qt.callLater(() => root.animateScroll = false); Qt.callLater(() => root.animateScroll = false);
if (row.flashHighlight !== undefined) if (row.flashHighlight !== undefined) // qmllint disable missing-property
row.flashHighlight(); row.flashHighlight(); // qmllint disable missing-property
return true; return true;
} }
@@ -155,11 +155,20 @@ ColumnLayout {
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: -topMargin Layout.topMargin: -topMargin
bottomMargin: Tokens.padding.extraLarge bottomMargin: Tokens.padding.extraLarge
topMargin: Tokens.padding.large
fadeAmount: 0.1
contentHeight: root.contentChild?.implicitHeight ?? 0 contentHeight: root.contentChild?.implicitHeight ?? 0
contentItem.children: [root.contentChild] contentItem.children: [root.contentChild]
fadeAmount: 0.1
topMargin: Tokens.padding.large rebound: Transition {
Anim {
properties: "x,y"
type: Anim.DefaultEffects
}
}
Behavior on contentY { Behavior on contentY {
enabled: root.animateScroll enabled: root.animateScroll
+74 -80
View File
@@ -13,9 +13,7 @@ import qs.Services
Item { Item {
id: wrapper id: wrapper
readonly property var currentScreen: screens.length > selectedScreenIndex ? screens[selectedScreenIndex] : null property int buttonRowHeight: Tokens.padding.large * 3
readonly property real maxZoom: 5.0
readonly property bool changesMade: { readonly property bool changesMade: {
cropRevision; 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; 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 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 var screens: []
property int selectedScreenIndex: 0 property int selectedScreenIndex: 0
property bool shouldBeActive: true 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 { function applyCrop(): void {
if (!cropRectLoader.item || !currentScreen) if (!cropRectLoader.item || !currentScreen)
return; return;
@@ -72,6 +62,10 @@ Item {
Qt.callLater(syncCropToScreen); Qt.callLater(syncCropToScreen);
} }
function sliderFromZoom(zoom: real): real {
return (1 - 1 / zoom) / (1 - 1 / maxZoom);
}
function syncCropToScreen(): void { function syncCropToScreen(): void {
if (cropRectLoader.item) if (cropRectLoader.item)
cropRectLoader.item.restoreFromData(); cropRectLoader.item.restoreFromData();
@@ -94,6 +88,10 @@ Item {
cropRect.clampToBounds(); cropRect.clampToBounds();
} }
function zoomFromSlider(t: real): real {
return 1 / (1 - t * (1 - 1 / maxZoom));
}
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
implicitHeight: shouldBeActive ? 430 : 0 implicitHeight: shouldBeActive ? 430 : 0
@@ -102,13 +100,16 @@ Item {
visible: opacity > 0 visible: opacity > 0
Behavior on opacity { Behavior on opacity {
Anim {} Anim {
}
} }
Behavior on scale { Behavior on scale {
Anim {} Anim {
}
} }
Behavior on y { Behavior on y {
Anim {} Anim {
}
} }
Component.onCompleted: { Component.onCompleted: {
@@ -140,12 +141,12 @@ Item {
required property var modelData required property var modelData
fillWidth: true fillWidth: true
font: Tokens.font.body.builders.large.weight(Font.DemiBold).build()
inactiveColor: isCurrent ? Colors.palette.m3primary : Qt.alpha(Colors.palette.m3surfaceContainer, 0.7) inactiveColor: isCurrent ? Colors.palette.m3primary : Qt.alpha(Colors.palette.m3surfaceContainer, 0.7)
inactiveOnColor: isCurrent ? Colors.palette.m3onPrimary : Qt.alpha(Colors.palette.m3onSurface, 0.7) inactiveOnColor: isCurrent ? Colors.palette.m3onPrimary : Qt.alpha(Colors.palette.m3onSurface, 0.7)
isRound: true isRound: true
shapeMorph: true shapeMorph: true
text: modelData.name text: modelData.name
font: Tokens.font.body.builders.large.weight(Font.DemiBold).build()
onClicked: wrapper.selectScreen(index) onClicked: wrapper.selectScreen(index)
} }
@@ -180,15 +181,15 @@ Item {
spacing: Tokens.spacing.medium spacing: Tokens.spacing.medium
IconTextButton { IconTextButton {
isRound: true enabled: wrapper.changesMade
icon: "close"
text: qsTr("Cancel")
font: Tokens.font.body.large font: Tokens.font.body.large
shapeMorph: true
horizontalPadding: Tokens.padding.large horizontalPadding: Tokens.padding.large
icon: "close"
inactiveColor: Colors.layer(Colors.palette.m3surfaceContainerHighest, 2) inactiveColor: Colors.layer(Colors.palette.m3surfaceContainerHighest, 2)
inactiveOnColor: Colors.palette.m3onSurfaceVariant inactiveOnColor: Colors.palette.m3onSurfaceVariant
enabled: wrapper.changesMade isRound: true
shapeMorph: true
text: qsTr("Cancel")
onClicked: { onClicked: {
wrapper.syncCropToScreen(); wrapper.syncCropToScreen();
@@ -196,15 +197,15 @@ Item {
} }
IconTextButton { IconTextButton {
isRound: true enabled: wrapper.changesMade
icon: "check"
text: qsTr("Apply")
font: Tokens.font.body.large font: Tokens.font.body.large
shapeMorph: true
horizontalPadding: Tokens.padding.large horizontalPadding: Tokens.padding.large
icon: "check"
inactiveColor: Colors.palette.m3secondary inactiveColor: Colors.palette.m3secondary
inactiveOnColor: Colors.palette.m3onSecondary inactiveOnColor: Colors.palette.m3onSecondary
enabled: wrapper.changesMade isRound: true
shapeMorph: true
text: qsTr("Apply")
onClicked: { onClicked: {
wrapper.applyCrop(); wrapper.applyCrop();
@@ -227,42 +228,37 @@ Item {
id: fitBox id: fitBox
readonly property real aspect: scaledImg.implicitHeight > 0 ? scaledImg.implicitWidth / scaledImg.implicitHeight : 1 readonly property real aspect: scaledImg.implicitHeight > 0 ? scaledImg.implicitWidth / scaledImg.implicitHeight : 1
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) implicitHeight: Math.round(fitW / aspect)
implicitWidth: Math.round(fitW) implicitWidth: Math.round(fitW)
x: Math.round((imageSlot.width - width) / 2)
y: Math.round((imageSlot.height - height) / 2)
radius: Tokens.rounding.largeIncreased radius: Tokens.rounding.largeIncreased
Loader { Loader {
anchors.centerIn: parent
opacity: scaledImg.status === Image.Ready ? 0 : 1
active: opacity > 0 active: opacity > 0
anchors.centerIn: parent
opacity: scaledImg.status === Image.Ready ? 0 : 1
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
sourceComponent: CustomRect { sourceComponent: CustomRect {
implicitWidth: loadingIndicator.implicitSize + Tokens.padding.large * 2
implicitHeight: loadingIndicator.implicitSize + Tokens.padding.large * 2
color: Colors.palette.m3primaryContainer color: Colors.palette.m3primaryContainer
implicitHeight: loadingIndicator.implicitSize + Tokens.padding.large * 2
implicitWidth: loadingIndicator.implicitSize + Tokens.padding.large * 2
radius: Tokens.rounding.full radius: Tokens.rounding.full
LoadingIndicator { LoadingIndicator {
id: loadingIndicator id: loadingIndicator
anchors.centerIn: parent anchors.centerIn: parent
containsIcon: true containsIcon: true
implicitSize: Math.min(fitBox.width, fitBox.height) * 0.3 implicitSize: Math.min(fitBox.width, fitBox.height) * 0.3
} }
} }
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
} }
Image { Image {
@@ -270,14 +266,16 @@ Item {
anchors.fill: parent anchors.fill: parent
asynchronous: true asynchronous: true
fillMode: Image.Stretch fillMode: Image.PreserveAspectFit
source: Wallpapers.current mipmap: true
sourceSize: {
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
return Qt.size(width * dpr, height * dpr);
}
retainWhileLoading: true
opacity: status === Image.Ready ? 1 : 0 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 { Behavior on opacity {
Anim { Anim {
@@ -297,7 +295,6 @@ Item {
property real aspectRatio: wrapper.currentScreen ? wrapper.currentScreen.width / wrapper.currentScreen.height : 1 property real aspectRatio: wrapper.currentScreen ? wrapper.currentScreen.width / wrapper.currentScreen.height : 1
readonly property real baseHeight: baseWidth / aspectRatio readonly property real baseHeight: baseWidth / aspectRatio
readonly property real baseWidth: { readonly property real baseWidth: {
let fittedHeight = imageH; let fittedHeight = imageH;
let fittedWidth = fittedHeight * aspectRatio; let fittedWidth = fittedHeight * aspectRatio;
@@ -309,7 +306,6 @@ Item {
return fittedWidth; return fittedWidth;
} }
readonly property real imageH: Math.round(fitBox.height) readonly property real imageH: Math.round(fitBox.height)
readonly property real imageW: Math.round(fitBox.width) readonly property real imageW: Math.round(fitBox.width)
readonly property real imageX: 0 readonly property real imageX: 0
@@ -327,6 +323,13 @@ Item {
y = Math.max(imageY, Math.min(y, imageY + imageH - height)); 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). // Where the rect should be according to saved data (same rules as a restore).
function savedTarget() { function savedTarget() {
const data = Wallpapers.getCrop(wrapper.currentScreen.name); const data = Wallpapers.getCrop(wrapper.currentScreen.name);
@@ -349,19 +352,13 @@ Item {
}; };
} }
function restoreFromData() { color: zoomSlider.dragging || mouse.pressed ? Qt.alpha(Colors.palette.m3tertiary, 0.3) : "transparent"
const t = savedTarget();
zoom = t.zoom;
x = t.x;
y = t.y;
}
height: Math.round(baseHeight / zoom) height: Math.round(baseHeight / zoom)
width: Math.round(baseWidth / zoom) width: Math.round(baseWidth / zoom)
color: zoomSlider.dragging || mouse.pressed ? Qt.alpha(Colors.palette.m3tertiary, 0.3) : "transparent"
Behavior on color { Behavior on color {
CAnim {} CAnim {
}
} }
Component.onCompleted: { Component.onCompleted: {
@@ -374,21 +371,19 @@ Item {
} }
ShaderEffect { 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 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 { function snap(v: real): real {
return Math.round(v * dpr) / dpr; return Math.round(v * dpr) / dpr;
} }
property color borderColor: Colors.palette.m3tertiary anchors.fill: parent
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
fragmentShader: Qt.resolvedUrl(`${Quickshell.shellDir}/assets/shaders/crop_sdf.frag.qsb`) fragmentShader: Qt.resolvedUrl(`${Quickshell.shellDir}/assets/shaders/crop_sdf.frag.qsb`)
} }
@@ -398,14 +393,6 @@ Item {
property real grabX: 0 property real grabX: 0
property real grabY: 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 { function beginGrab(mx, my): void {
const r = cropRectLoader.item; const r = cropRectLoader.item;
if (!r) 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 { function updateCrop(mx, my): void {
if (!cropRectLoader.item) if (!cropRectLoader.item)
return; return;
@@ -439,11 +434,10 @@ Item {
} }
anchors.fill: parent anchors.fill: parent
cursorShape: pressed ? Qt.ClosedHandCursor : insideCropRect(mouseX, mouseY) ? Qt.OpenHandCursor : Qt.PointingHandCursor
hoverEnabled: true hoverEnabled: true
preventStealing: true preventStealing: true
cursorShape: pressed ? Qt.ClosedHandCursor : insideCropRect(mouseX, mouseY) ? Qt.OpenHandCursor : Qt.PointingHandCursor
onPositionChanged: mouse => { onPositionChanged: mouse => {
if (pressed) { if (pressed) {
updateCrop(mouse.x, mouse.y); updateCrop(mouse.x, mouse.y);
+8 -16
View File
@@ -81,14 +81,10 @@ VerticalFadeFlickable {
topLeftRadius: stateLayer.pressed ? Tokens.rounding.medium : isCurrentPage ? Tokens.rounding.extraLarge : isCategoryStart ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall topLeftRadius: stateLayer.pressed ? Tokens.rounding.medium : isCurrentPage ? Tokens.rounding.extraLarge : isCategoryStart ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
topRightRadius: stateLayer.pressed ? Tokens.rounding.medium : isCurrentPage ? Tokens.rounding.extraLarge : isCategoryStart ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall topRightRadius: stateLayer.pressed ? Tokens.rounding.medium : isCurrentPage ? Tokens.rounding.extraLarge : isCategoryStart ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
RadiusBehavior on bottomLeftRadius { RadiusBehavior on bottomLeftRadius {}
} RadiusBehavior on bottomRightRadius {}
RadiusBehavior on bottomRightRadius { RadiusBehavior on topLeftRadius {}
} RadiusBehavior on topRightRadius {}
RadiusBehavior on topLeftRadius {
}
RadiusBehavior on topRightRadius {
}
StateLayer { StateLayer {
id: stateLayer id: stateLayer
@@ -264,14 +260,10 @@ VerticalFadeFlickable {
topRightRadius: layer.pressed ? Tokens.rounding.largeIncreased : isFirst ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall topRightRadius: layer.pressed ? Tokens.rounding.largeIncreased : isFirst ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall
width: cardList.width width: cardList.width
RadiusBehavior on bottomLeftRadius { RadiusBehavior on bottomLeftRadius {}
} RadiusBehavior on bottomRightRadius {}
RadiusBehavior on bottomRightRadius { RadiusBehavior on topLeftRadius {}
} RadiusBehavior on topRightRadius {}
RadiusBehavior on topLeftRadius {
}
RadiusBehavior on topRightRadius {
}
ColumnLayout { ColumnLayout {
id: resultLayout id: resultLayout
+29 -46
View File
@@ -11,6 +11,7 @@ import qs.Modules.Settings.Pages.Audio
import qs.Modules.Settings.Pages.Apps import qs.Modules.Settings.Pages.Apps
import qs.Modules.Settings.Pages.Panels import qs.Modules.Settings.Pages.Panels
import qs.Modules.Settings.Pages.Panels.Bar import qs.Modules.Settings.Pages.Panels.Bar
import qs.Modules.Settings.Pages.Panels.Sidebar
import qs.Modules.Settings.Pages.Services import qs.Modules.Settings.Pages.Services
import qs.Services import qs.Services
@@ -23,13 +24,11 @@ QtObject {
// Wallpaper & style // Wallpaper & style
StackPage { StackPage {
Component { Component {
WallpaperPage { WallpaperPage {}
}
} }
Component { Component {
WallpaperSelect { WallpaperSelect {}
}
} }
Component { Component {
@@ -41,32 +40,27 @@ QtObject {
// Screenshot // Screenshot
StackPage { StackPage {
Component { Component {
ScreenshotPage { ScreenshotPage {}
}
} }
} }
}, },
// Connectivity // Connectivity
Component { Component {
PlaceholderComp { PlaceholderComp {}
}
}, },
Component { Component {
PlaceholderComp { PlaceholderComp {}
}
}, },
Component { Component {
// Audio // Audio
StackPage { StackPage {
Component { Component {
AudioPage { AudioPage {}
}
} }
Component { Component {
AppVolumes { AppVolumes {}
}
} }
} }
}, },
@@ -75,49 +69,45 @@ QtObject {
Component { Component {
StackPage { StackPage {
Component { Component {
PanelsPage { PanelsPage {}
}
} }
Component { Component {
BarPanel { BarPanel {}
}
} }
Component { Component {
DashboardPanel { DashboardPanel {}
}
} }
Component { Component {
ResourcesPanel { ResourcesPanel {}
}
} }
Component { Component {
LauncherPanel { LauncherPanel {}
}
} }
Component { Component {
SidebarPanel { SidebarPanel {}
}
} }
// Bar sub pages // Bar sub pages
Component { Component {
BarTray { BarTray {}
}
} }
Component { Component {
BarStatusIcons { BarStatusIcons {}
}
} }
Component { Component {
BarClock { BarClock {}
} }
// Sidebar sub pages
Component {
SidebarLlm {}
} }
} }
}, },
@@ -126,18 +116,15 @@ QtObject {
Component { Component {
StackPage { StackPage {
Component { Component {
AppsPage { AppsPage {}
}
} }
Component { Component {
AllApps { AllApps {}
}
} }
Component { Component {
AppInfo { AppInfo {}
}
} }
} }
}, },
@@ -146,13 +133,11 @@ QtObject {
Component { Component {
StackPage { StackPage {
Component { Component {
ServicesPage { ServicesPage {}
}
} }
Component { Component {
NotificationsPage { NotificationsPage {}
}
} }
} }
}, },
@@ -161,15 +146,13 @@ QtObject {
Component { Component {
StackPage { StackPage {
Component { Component {
AboutPage { AboutPage {}
}
} }
} }
} }
] ]
readonly property Component placeholderComp: Component { readonly property Component placeholderComp: Component {
PlaceholderComp { PlaceholderComp {}
}
} }
component PlaceholderComp: Item { component PlaceholderComp: Item {
-12
View File
@@ -42,18 +42,6 @@ Item {
objectName: "PageContainer" objectName: "PageContainer"
Component.onCompleted: root.loadPage(root.sState.currentPageIdx) 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 { Connections {
+2 -2
View File
@@ -52,11 +52,11 @@ PageBase {
anchors.leftMargin: Tokens.padding.largeIncreased anchors.leftMargin: Tokens.padding.largeIncreased
anchors.margins: Tokens.padding.small anchors.margins: Tokens.padding.small
anchors.rightMargin: Tokens.padding.largeIncreased anchors.rightMargin: Tokens.padding.largeIncreased
spacing: Tokens.spacing.small spacing: Tokens.spacing.medium
IconImage { IconImage {
asynchronous: true 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") source: Quickshell.iconPath(appItem.modelData.icon, "image-missing")
} }
@@ -55,8 +55,8 @@ PageBase {
enabled: Object.keys(model).length > 0 enabled: Object.keys(model).length > 0
header: qsTr("Add new entry") header: qsTr("Add new entry")
icon: "add" icon: "add"
label: qsTr("Add entry")
last: true last: true
label: qsTr("Add entry")
model: { model: {
const present = new Set(Config.bar.tray.statusIcons.values.map(item => item.id)); const present = new Set(Config.bar.tray.statusIcons.values.map(item => item.id));
return Object.keys(root.builtinIcons).filter(id => !present.has(id)).map(id => ({ return Object.keys(root.builtinIcons).filter(id => !present.has(id)).map(id => ({
@@ -0,0 +1,112 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import ZShell.Config
import qs.Components
import qs.Modules.Settings.Common
PageBase {
id: root
readonly property var schemes: [
{
id: "oneDark",
label: qsTr("One Dark")
},
{
id: "nord",
label: qsTr("Nord")
},
{
id: "dracula",
label: qsTr("Dracula")
},
{
id: "githubDark",
label: qsTr("Github Dark")
},
{
id: "solarizedDark",
label: qsTr("Solarized Dark")
},
{
id: "monokai",
label: qsTr("Monokai")
},
{
id: "gruvboxDark",
label: qsTr("Gruvbox Dark")
},
{
id: "catppuccinMocha",
label: qsTr("Catppuccin Mocha")
},
{
id: "tokyoNight",
label: qsTr("Tokyo Night")
},
{
id: "ayuDark",
label: qsTr("Ayu Dark")
},
{
id: "palenight",
label: qsTr("Pale Night")
}
]
isSubPage: true
title: qsTr("Sidebar")
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
spacing: Tokens.spacing.extraSmall / 2
width: root.cappedWidth
SectionHeader {
first: true
text: qsTr("General")
}
ToggleRow {
settingAnchor: "panels-sidebar-llm-enabled"
checked: Config.llm.enabled
text: qsTr("Enabled")
first: true
last: true
onToggled: Config.llm.enabled = checked
}
SectionHeader {
text: qsTr("Appearance")
}
DialogSelectButton {
id: selectScheme
acceptLabel: qsTr("Confirm")
settingAnchor: "panels-sidebar-llm-scheme"
enabled: Object.keys(model).length > 0
header: qsTr("Select scheme")
first: true
last: true
icon: "add"
label: qsTr("Color scheme")
subtext: qsTr("Select the color scheme used for code blocks")
model: root.schemes
rootParent: root.flickable
initialSelect: Config.llm.appearance.scheme
onAccepted: {
if (!selectedItem)
return;
Config.llm.appearance.scheme = selectedItem;
}
}
}
}
@@ -93,6 +93,7 @@ PageBase {
} }
settingAnchor: "panels-sidebar-quick-toggles-add-item" settingAnchor: "panels-sidebar-quick-toggles-add-item"
label: qsTr("Add entry") label: qsTr("Add entry")
last: true
model: { model: {
const present = new Set(Config.utilities.quickToggles.values.map(item => item.id)); const present = new Set(Config.utilities.quickToggles.values.map(item => item.id));
return Object.keys(root.builtinIcons).filter(id => !present.has(id)).map(id => ({ return Object.keys(root.builtinIcons).filter(id => !present.has(id)).map(id => ({
@@ -112,5 +113,19 @@ PageBase {
}); });
} }
} }
SectionHeader {
text: qsTr("AI chat")
}
NavRow {
text: qsTr("AI chat")
settingAnchor: "panels-sidebar-llm"
icon: "robot_2"
first: true
last: true
onClicked: root.sState.openSubPage(9)
}
} }
} }
+2 -14
View File
@@ -25,18 +25,6 @@ PageBase {
text: qsTr("None") 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") title: qsTr("Services")
@@ -211,7 +199,7 @@ PageBase {
} }
SelectRow { SelectRow {
active: root.gpuItems[root.gpuKeyToIndex(Config.services.gpuType)] active: root.gpuItems[Config.services.gpuType]
last: true last: true
menuItems: root.gpuItems menuItems: root.gpuItems
menuOnTop: true menuOnTop: true
@@ -219,7 +207,7 @@ PageBase {
subtext: Gpu.name ? qsTr("Monitoring: %1").arg(Gpu.name) : qsTr("Override for GPU type") subtext: Gpu.name ? qsTr("Monitoring: %1").arg(Gpu.name) : qsTr("Override for GPU type")
text: qsTr("GPU") text: qsTr("GPU")
onSelected: item => Config.services.gpuType = root.gpuValues[root.gpuItems.indexOf(item)] onSelected: item => Config.services.gpuType = root.gpuItems.indexOf(item)
} }
} }
} }
+84 -59
View File
@@ -1,66 +1,24 @@
pragma Singleton pragma Singleton
import "../../scripts/fzf.js" as Fzf import "../../scripts/fzf.js" as Fzf
import "../../scripts/settings-indexer.js" as SettingsIndexer
import QtQuick import QtQuick
import Quickshell import Quickshell
import ZShell import ZShell
import ZShell.Config import ZShell.Config
import qs.Paths
Singleton { Singleton {
id: root id: root
property var fzfFinder: null property var inverted: ({})
property var ranking: ({})
readonly property var highlightCache: ({ readonly property var highlightCache: ({
"search": "", "search": "",
"pattern": null "pattern": null
}) })
property var inverted: ({}) property var fzfFinder: null
property var ranking: ({}) readonly property string cachePath: Paths.cache + "/settings-index.json"
function highlight(text: string, search: string, colour: color): string {
const escaped = text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
if (search.length === 0)
return escaped;
const cache = root.highlightCache;
if (search !== cache.search) {
const tokens = tokenize(search);
cache.search = search;
if (tokens.length === 0)
cache.pattern = null;
else {
const escapedTokens = tokens.map(t => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
cache.pattern = new RegExp("\\b(" + escapedTokens.join("|") + ")", "gi");
}
}
const pattern = cache.pattern;
if (!pattern)
return escaped;
pattern.lastIndex = 0;
if (!pattern.test(escaped))
return escaped;
pattern.lastIndex = 0;
return escaped.replace(pattern, `<font color="${colour}">$1</font>`);
}
function lookup(token: string): var {
const result = ({});
const exact = root.inverted[token] !== undefined;
const keys = exact ? [token] : Object.keys(root.inverted).filter(k => k.startsWith(token));
for (const key of keys) {
const rank = root.ranking[key] ?? ({});
for (const id of root.inverted[key]) {
const w = rank[id] ?? 0.1;
if (result[id] === undefined || w > result[id])
result[id] = w;
}
}
return result;
}
function query(search: string): list<QtObject> { function query(search: string): list<QtObject> {
const tokens = root.tokenize(search); const tokens = root.tokenize(search);
@@ -70,7 +28,7 @@ Singleton {
const scores = ({}); const scores = ({});
const hitCounts = ({}); const hitCounts = ({});
for (const token of tokens) { for (const token of tokens) {
const matches = root.lookup(token); const matches = root.lookup(token); // { id: weight }
for (const id in matches) { for (const id in matches) {
scores[id] = (scores[id] ?? 0) + matches[id]; scores[id] = (scores[id] ?? 0) + matches[id];
hitCounts[id] = (hitCounts[id] ?? 0) + 1; hitCounts[id] = (hitCounts[id] ?? 0) + 1;
@@ -103,13 +61,76 @@ Singleton {
return out; return out;
} }
function lookup(token: string): var {
const result = ({});
const exact = root.inverted[token] !== undefined;
const keys = exact ? [token] : Object.keys(root.inverted).filter(k => k.startsWith(token));
for (const key of keys) {
const rank = root.ranking[key] ?? ({});
for (const id of root.inverted[key]) {
const w = rank[id] ?? 0.1;
if (result[id] === undefined || w > result[id])
result[id] = w;
}
}
return result;
}
function tokenize(text: string): var { function tokenize(text: string): var {
return text.toLowerCase().split(/[^a-z0-9]+/).filter(t => t.length > 0); return text.toLowerCase().split(/[^a-z0-9]+/).filter(t => t.length > 0);
} }
function highlight(text: string, search: string, colour: color): string {
const escaped = text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
if (search.length === 0)
return escaped;
const cache = root.highlightCache;
if (search !== cache.search) {
const tokens = root.tokenize(search);
cache.search = search;
if (tokens.length === 0) {
cache.pattern = null;
} else {
const escapedTokens = tokens.map(t => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
cache.pattern = new RegExp("\\b(" + escapedTokens.join("|") + ")", "gi");
}
}
const pattern = cache.pattern;
if (!pattern)
return escaped;
pattern.lastIndex = 0;
if (!pattern.test(escaped))
return escaped;
pattern.lastIndex = 0;
return escaped.replace(pattern, `<font color="${colour}">$1</font>`);
}
function loadIndex(): var {
const revision = ZUtils.gitRevision();
const cached = ZUtils.readTextFile(cachePath);
if (cached) {
try {
const parsed = JSON.parse(cached);
if (parsed.version === 3 && revision && parsed.revision === revision && parsed.locale === Qt.locale().name)
return parsed;
} catch (e) {}
}
const data = SettingsIndexer.buildIndex(`${Quickshell.shellDir}/Modules/Settings`, p => ZUtils.readTextFile(p), (d, s) => ZUtils.listFiles(d, s), (ctx, text) => qsTranslate(ctx, text));
data.revision = revision;
data.locale = Qt.locale().name;
ZUtils.writeTextFile(cachePath, JSON.stringify(data));
console.log(`SettingsSearcher: indexed ${data.entries.length} settings (revision ${revision || "unknown"})`);
return data;
}
Component.onCompleted: { Component.onCompleted: {
try { try {
const data = JSON.parse(ZUtils.settingsIndex()); const data = root.loadIndex();
entries.model = data.entries; entries.model = data.entries;
root.inverted = data.inverted ?? {}; root.inverted = data.inverted ?? {};
root.ranking = data.ranking ?? {}; root.ranking = data.ranking ?? {};
@@ -122,6 +143,7 @@ Singleton {
limit: 25 limit: 25
}); });
} catch (e) { } catch (e) {
console.warn("SettingsSearcher: failed to build settings index:", e);
entries.model = []; entries.model = [];
root.inverted = {}; root.inverted = {};
root.ranking = {}; root.ranking = {};
@@ -132,22 +154,25 @@ Singleton {
Variants { Variants {
id: entries id: entries
SettingEntry { SettingEntry {}
}
} }
component SettingEntry: QtObject { component SettingEntry: QtObject {
readonly property string anchor: modelData.anchor ?? "" required property var modelData
readonly property int pageIdx: modelData.pageIdx
readonly property var subPath: modelData.subPath
readonly property var targetSubPath: modelData.targetSubPath ?? []
readonly property var crumbIcons: modelData.crumbIcons readonly property var crumbIcons: modelData.crumbIcons
readonly property var crumbLabels: modelData.crumbLabels readonly property var crumbLabels: modelData.crumbLabels
readonly property bool isToggle: togglePath.length > 0
required property var modelData
readonly property int pageIdx: modelData.pageIdx
readonly property string section: modelData.section ?? ""
readonly property var subPath: modelData.subPath
readonly property string subtext: modelData.subtext ?? ""
readonly property string title: modelData.title readonly property string title: modelData.title
readonly property string section: modelData.section ?? ""
readonly property string subtext: modelData.subtext ?? ""
readonly property string anchor: modelData.anchor ?? ""
readonly property string icon: modelData.icon ?? ""
readonly property string togglePath: modelData.togglePath ?? "" readonly property string togglePath: modelData.togglePath ?? ""
readonly property bool isToggle: togglePath.length > 0
readonly property bool toggleValue: { readonly property bool toggleValue: {
if (!isToggle) if (!isToggle)
return false; return false;
+17
View File
@@ -11,6 +11,18 @@ Scope {
readonly property bool hasFullscreen: Hypr.focusedWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen === 2) ?? false readonly property bool hasFullscreen: Hypr.focusedWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen === 2) ?? false
property bool launcherInterrupted property bool launcherInterrupted
CustomShortcut {
description: "Open screenshot"
name: "screenshot"
onPressed: {
const states = ShellState.allStates();
for (const s of states) {
s.screenshot = true;
}
}
}
CustomShortcut { CustomShortcut {
description: "Toggle launcher" description: "Toggle launcher"
name: "toggle-launcher" name: "toggle-launcher"
@@ -26,6 +38,7 @@ Scope {
} }
CustomShortcut { CustomShortcut {
description: "Toggle drawing"
name: "toggle-drawing" name: "toggle-drawing"
onPressed: { onPressed: {
@@ -35,6 +48,7 @@ Scope {
} }
CustomShortcut { CustomShortcut {
description: "Toggle sidebar"
name: "toggle-nc" name: "toggle-nc"
onPressed: { onPressed: {
@@ -44,6 +58,7 @@ Scope {
} }
CustomShortcut { CustomShortcut {
description: "Show On-Screen-Display"
name: "show-osd" name: "show-osd"
onPressed: { onPressed: {
@@ -53,6 +68,7 @@ Scope {
} }
CustomShortcut { CustomShortcut {
description: "Toggle settings"
name: "toggle-settings" name: "toggle-settings"
onPressed: { onPressed: {
@@ -62,6 +78,7 @@ Scope {
} }
CustomShortcut { CustomShortcut {
description: "Toggle clipboard"
name: "toggle-clipboard" name: "toggle-clipboard"
onPressed: { 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 {
}
}
}
}
}
+44
View File
@@ -1 +1,45 @@
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)
pkg_check_modules(Cava IMPORTED_TARGET libcava QUIET)
pkg_check_modules(GLIB REQUIRED glib-2.0 gobject-2.0 gio-2.0)
if(NOT Cava_FOUND)
pkg_check_modules(Cava IMPORTED_TARGET cava REQUIRED)
endif()
include(cmake/sensorslib.cmake)
set(QT_QML_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/qml")
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")
add_subdirectory(ZShell) add_subdirectory(ZShell)
+17 -13
View File
@@ -1,20 +1,24 @@
qml_module(ZShell-blobs qml_module(ZShell-blobs
URI ZShell.Blobs URI ZShell.Blobs
SOURCES SOURCES
blobgroup.cpp blobgroup.cpp
blobshape.cpp blobshape.cpp
blobrect.cpp blobrect.cpp
blobinvertedrect.cpp blobinvertedrect.cpp
blobmaterial.cpp blobgroup2.cpp
LIBRARIES blobshape2.cpp
Qt::Quick blobrect2.cpp
blobinvertedrect2.cpp
blobmaterial.cpp
LIBRARIES
Qt::Quick
) )
qt_add_shaders(ZShell-blobs "blob_shaders" qt_add_shaders(ZShell-blobs "blob_shaders"
BATCHABLE OPTIMIZED NOHLSL NOMSL BATCHABLE OPTIMIZED NOHLSL NOMSL
GLSL "300es,330" GLSL "300es,330"
PREFIX "/" PREFIX "/"
FILES FILES
shaders/blob.frag shaders/blob.frag
shaders/blob.vert 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); Q_UNUSED(oldMaterial);
auto* mat = static_cast<BlobMaterial*>(newMaterial); auto* mat = static_cast<BlobMaterial*>(newMaterial);
QByteArray* buf = state.uniformData(); QByteArray* buf = state.uniformData();
Q_ASSERT(buf->size() >= 1440); Q_ASSERT(buf->size() >= 1472);
if (state.isMatrixDirty()) { if (state.isMatrixDirty()) {
const QMatrix4x4 m = state.combinedMatrix(); const QMatrix4x4 m = state.combinedMatrix();
@@ -56,7 +56,7 @@ bool BlobMaterialShader::updateUniformData(
// Rect count (offset 88) // Rect count (offset 88)
memcpy(buf->data() + 88, &mat->m_rectCount, 4); 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); memcpy(buf->data() + 92, &mat->m_myIndex, 4);
// Color as vec4 (offset 96, 16 bytes) // Color as vec4 (offset 96, 16 bytes)
@@ -100,5 +100,10 @@ bool BlobMaterialShader::updateUniformData(
memcpy(buf->data() + base + 64, r.radius, 16); 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; return true;
} }
+5
View File
@@ -32,12 +32,17 @@ class BlobMaterial : public QSGMaterial {
float m_paddedH = 0; float m_paddedH = 0;
float m_smoothFactor = 32.0f; float m_smoothFactor = 32.0f;
int m_rectCount = 0; int m_rectCount = 0;
// -2 means no owner test: the single group draw writes every pixel it covers.
int m_myIndex = -2; int m_myIndex = -2;
QColor m_color{0x44, 0x88, 0xff}; QColor m_color{0x44, 0x88, 0xff};
int m_hasInverted = 0; int m_hasInverted = 0;
float m_invertedRadius = 0; float m_invertedRadius = 0;
float m_invertedOuter[4] = {}; float m_invertedOuter[4] = {};
float m_invertedInner[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] = {}; BlobRectData m_rects[16] = {};
}; };

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