From e68f585b9f0baa895fd81755bbdb2d2a98521ec6 Mon Sep 17 00:00:00 2001 From: reya Date: Thu, 15 Jan 2026 08:59:40 +0700 Subject: [PATCH 01/30] update deps --- Cargo.lock | 91 +++++++++++++++++++++++++++--------------------------- 1 file changed, 46 insertions(+), 45 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5192d4d..7c8d9cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -567,9 +567,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.15.2" +version = "1.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a88aab2464f1f25453baa7a07c84c5b7684e274054ba06817f382357f77a288" +checksum = "e84ce723ab67259cfeb9877c6a639ee9eb7a27b28123abd71db7f0d5d0cc9d86" dependencies = [ "aws-lc-sys", "zeroize", @@ -577,9 +577,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.35.0" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45afffdee1e7c9126814751f88dddc747f41d91da16c9551a0f1e8a11e788a1" +checksum = "43a442ece363113bd4bd4c8b18977a7798dd4d3c3383f34fb61936960e8f4ad8" dependencies = [ "cc", "cmake", @@ -1056,9 +1056,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" dependencies = [ "iana-time-zone", "js-sys", @@ -1179,7 +1179,7 @@ dependencies = [ [[package]] name = "collections" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#acfc71a42304a19d1c0b5753d3513c0ec0fa1547" +source = "git+https://github.com/zed-industries/zed#ceecf82287b4f9323b4ecbfb388a147361db7aaf" dependencies = [ "indexmap", "rustc-hash 2.1.1", @@ -1607,7 +1607,7 @@ dependencies = [ [[package]] name = "derive_refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#acfc71a42304a19d1c0b5753d3513c0ec0fa1547" +source = "git+https://github.com/zed-industries/zed#ceecf82287b4f9323b4ecbfb388a147361db7aaf" dependencies = [ "proc-macro2", "quote", @@ -2517,7 +2517,7 @@ dependencies = [ [[package]] name = "gpui" version = "0.2.2" -source = "git+https://github.com/zed-industries/zed#acfc71a42304a19d1c0b5753d3513c0ec0fa1547" +source = "git+https://github.com/zed-industries/zed#ceecf82287b4f9323b4ecbfb388a147361db7aaf" dependencies = [ "anyhow", "as-raw-xcb-connection", @@ -2619,7 +2619,7 @@ dependencies = [ [[package]] name = "gpui_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#acfc71a42304a19d1c0b5753d3513c0ec0fa1547" +source = "git+https://github.com/zed-industries/zed#ceecf82287b4f9323b4ecbfb388a147361db7aaf" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -2630,7 +2630,7 @@ dependencies = [ [[package]] name = "gpui_tokio" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#acfc71a42304a19d1c0b5753d3513c0ec0fa1547" +source = "git+https://github.com/zed-industries/zed#ceecf82287b4f9323b4ecbfb388a147361db7aaf" dependencies = [ "anyhow", "gpui", @@ -2852,7 +2852,7 @@ dependencies = [ [[package]] name = "http_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#acfc71a42304a19d1c0b5753d3513c0ec0fa1547" +source = "git+https://github.com/zed-industries/zed#ceecf82287b4f9323b4ecbfb388a147361db7aaf" dependencies = [ "anyhow", "async-compression", @@ -2877,7 +2877,7 @@ dependencies = [ [[package]] name = "http_client_tls" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#acfc71a42304a19d1c0b5753d3513c0ec0fa1547" +source = "git+https://github.com/zed-industries/zed#ceecf82287b4f9323b4ecbfb388a147361db7aaf" dependencies = [ "rustls", "rustls-platform-verifier", @@ -3307,9 +3307,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.83" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" dependencies = [ "once_cell", "wasm-bindgen", @@ -3638,7 +3638,7 @@ dependencies = [ [[package]] name = "media" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#acfc71a42304a19d1c0b5753d3513c0ec0fa1547" +source = "git+https://github.com/zed-industries/zed#ceecf82287b4f9323b4ecbfb388a147361db7aaf" dependencies = [ "anyhow", "bindgen", @@ -4498,7 +4498,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perf" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#acfc71a42304a19d1c0b5753d3513c0ec0fa1547" +source = "git+https://github.com/zed-industries/zed#ceecf82287b4f9323b4ecbfb388a147361db7aaf" dependencies = [ "collections", "serde", @@ -5133,7 +5133,7 @@ dependencies = [ [[package]] name = "refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#acfc71a42304a19d1c0b5753d3513c0ec0fa1547" +source = "git+https://github.com/zed-industries/zed#ceecf82287b4f9323b4ecbfb388a147361db7aaf" dependencies = [ "derive_refineable", ] @@ -5232,7 +5232,7 @@ dependencies = [ [[package]] name = "reqwest_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#acfc71a42304a19d1c0b5753d3513c0ec0fa1547" +source = "git+https://github.com/zed-industries/zed#ceecf82287b4f9323b4ecbfb388a147361db7aaf" dependencies = [ "anyhow", "bytes", @@ -5286,7 +5286,7 @@ dependencies = [ [[package]] name = "rope" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#acfc71a42304a19d1c0b5753d3513c0ec0fa1547" +source = "git+https://github.com/zed-industries/zed#ceecf82287b4f9323b4ecbfb388a147361db7aaf" dependencies = [ "arrayvec", "log", @@ -5306,9 +5306,9 @@ checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" [[package]] name = "rust-embed" -version = "8.10.0" +version = "8.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f783a9e226b5319beefe29d45941f559ace8b56801bb8355be17eea277fc8272" +checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27" dependencies = [ "rust-embed-impl", "rust-embed-utils", @@ -5317,9 +5317,9 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "8.10.0" +version = "8.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "303d4e979140595f1d824b3dd53a32684835fa32425542056826521ac279f538" +checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa" dependencies = [ "proc-macro2", "quote", @@ -5330,9 +5330,9 @@ dependencies = [ [[package]] name = "rust-embed-utils" -version = "8.10.0" +version = "8.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6b4ab509cae251bd524d2425d746b0af0018f5a81fc1eaecdd4e661c8ab3a0" +checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1" dependencies = [ "globset", "sha2", @@ -5565,7 +5565,7 @@ dependencies = [ [[package]] name = "scheduler" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#acfc71a42304a19d1c0b5753d3513c0ec0fa1547" +source = "git+https://github.com/zed-industries/zed#ceecf82287b4f9323b4ecbfb388a147361db7aaf" dependencies = [ "async-task", "backtrace", @@ -6155,7 +6155,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sum_tree" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#acfc71a42304a19d1c0b5753d3513c0ec0fa1547" +source = "git+https://github.com/zed-industries/zed#ceecf82287b4f9323b4ecbfb388a147361db7aaf" dependencies = [ "arrayvec", "log", @@ -7113,7 +7113,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "util" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#acfc71a42304a19d1c0b5753d3513c0ec0fa1547" +source = "git+https://github.com/zed-industries/zed#ceecf82287b4f9323b4ecbfb388a147361db7aaf" dependencies = [ "anyhow", "async-fs", @@ -7149,7 +7149,7 @@ dependencies = [ [[package]] name = "util_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#acfc71a42304a19d1c0b5753d3513c0ec0fa1547" +source = "git+https://github.com/zed-industries/zed#ceecf82287b4f9323b4ecbfb388a147361db7aaf" dependencies = [ "perf", "quote", @@ -7302,9 +7302,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" dependencies = [ "cfg-if", "once_cell", @@ -7315,11 +7315,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.56" +version = "0.4.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" dependencies = [ "cfg-if", + "futures-util", "js-sys", "once_cell", "wasm-bindgen", @@ -7328,9 +7329,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -7338,9 +7339,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" dependencies = [ "bumpalo", "proc-macro2", @@ -7351,9 +7352,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.106" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" dependencies = [ "unicode-ident", ] @@ -7471,9 +7472,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.83" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" dependencies = [ "js-sys", "wasm-bindgen", @@ -8624,7 +8625,7 @@ dependencies = [ [[package]] name = "zlog" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#acfc71a42304a19d1c0b5753d3513c0ec0fa1547" +source = "git+https://github.com/zed-industries/zed#ceecf82287b4f9323b4ecbfb388a147361db7aaf" dependencies = [ "anyhow", "chrono", @@ -8641,7 +8642,7 @@ checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea" [[package]] name = "ztracing" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#acfc71a42304a19d1c0b5753d3513c0ec0fa1547" +source = "git+https://github.com/zed-industries/zed#ceecf82287b4f9323b4ecbfb388a147361db7aaf" dependencies = [ "tracing", "tracing-subscriber", @@ -8652,7 +8653,7 @@ dependencies = [ [[package]] name = "ztracing_macro" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#acfc71a42304a19d1c0b5753d3513c0ec0fa1547" +source = "git+https://github.com/zed-industries/zed#ceecf82287b4f9323b4ecbfb388a147361db7aaf" [[package]] name = "zune-core" -- 2.49.1 From 4c4fe0cc0cc022d0394374cdd80e516185901783 Mon Sep 17 00:00:00 2001 From: reya Date: Fri, 16 Jan 2026 06:14:43 +0700 Subject: [PATCH 02/30] revamp icon pack --- .github/workflows/rust.yml | 2 +- Cargo.lock | 38 ++++++------ assets/icons/arrow-left.svg | 17 +++++- assets/icons/arrow-right.svg | 4 +- assets/icons/arrows-in.svg | 1 - assets/icons/boom.svg | 3 + assets/icons/caret-down-fill.svg | 1 - assets/icons/caret-down.svg | 4 +- assets/icons/caret-right.svg | 4 +- assets/icons/caret-up.svg | 4 +- assets/icons/check-circle-fill.svg | 1 - assets/icons/check-circle.svg | 4 +- assets/icons/check.svg | 4 +- assets/icons/close-circle-fill.svg | 4 +- assets/icons/close-circle.svg | 4 +- assets/icons/close.svg | 4 +- assets/icons/copy.svg | 4 +- assets/icons/door.svg | 3 + assets/icons/edit.svg | 4 -- assets/icons/ellipsis.svg | 5 +- assets/icons/emoji-fill.svg | 3 - assets/icons/emoji.svg | 3 + assets/icons/encryption.svg | 3 - assets/icons/eye.svg | 3 + assets/icons/group.svg | 3 - assets/icons/info.svg | 4 +- assets/icons/link.svg | 3 + assets/icons/logout.svg | 3 - assets/icons/minimize.svg | 3 - assets/icons/moon.svg | 4 +- assets/icons/open-url.svg | 3 - assets/icons/panel-left-open.svg | 3 - assets/icons/panel-left.svg | 3 - assets/icons/panel-right-open.svg | 3 - assets/icons/panel-right.svg | 3 - assets/icons/plus-circle-fill.svg | 1 - assets/icons/plus-circle.svg | 3 + assets/icons/plus-fill.svg | 3 - assets/icons/plus.svg | 4 +- assets/icons/refresh.svg | 4 -- assets/icons/relay.svg | 3 + assets/icons/reply.svg | 4 +- assets/icons/report.svg | 3 - assets/icons/resize-corner.svg | 9 --- assets/icons/search.svg | 4 +- assets/icons/server.svg | 4 -- assets/icons/settings.svg | 5 +- assets/icons/signal.svg | 3 - assets/icons/sun.svg | 4 +- assets/icons/upload.svg | 4 +- assets/icons/warning.svg | 4 +- assets/icons/zoom.svg | 58 +++++++++++++++++++ crates/chat_ui/src/lib.rs | 2 +- crates/coop/src/chatspace.rs | 27 +-------- crates/coop/src/new_identity/backup.rs | 4 +- crates/coop/src/new_identity/mod.rs | 2 +- crates/coop/src/sidebar/mod.rs | 2 +- crates/coop/src/user/mod.rs | 2 +- crates/coop/src/user/viewer.rs | 4 +- crates/coop/src/views/compose.rs | 4 +- crates/coop/src/views/screening.rs | 4 +- crates/coop/src/views/setup_relay.rs | 2 +- crates/ui/src/dock_area/tab_panel.rs | 2 +- crates/ui/src/icon.rs | 80 ++++++-------------------- crates/ui/src/menu/popup_menu.rs | 2 +- 65 files changed, 193 insertions(+), 226 deletions(-) delete mode 100644 assets/icons/arrows-in.svg create mode 100644 assets/icons/boom.svg delete mode 100644 assets/icons/caret-down-fill.svg delete mode 100644 assets/icons/check-circle-fill.svg create mode 100644 assets/icons/door.svg delete mode 100644 assets/icons/edit.svg delete mode 100644 assets/icons/emoji-fill.svg create mode 100644 assets/icons/emoji.svg delete mode 100644 assets/icons/encryption.svg create mode 100644 assets/icons/eye.svg delete mode 100644 assets/icons/group.svg create mode 100644 assets/icons/link.svg delete mode 100644 assets/icons/logout.svg delete mode 100644 assets/icons/minimize.svg delete mode 100644 assets/icons/open-url.svg delete mode 100644 assets/icons/panel-left-open.svg delete mode 100644 assets/icons/panel-left.svg delete mode 100644 assets/icons/panel-right-open.svg delete mode 100644 assets/icons/panel-right.svg delete mode 100644 assets/icons/plus-circle-fill.svg create mode 100644 assets/icons/plus-circle.svg delete mode 100644 assets/icons/plus-fill.svg delete mode 100644 assets/icons/refresh.svg create mode 100644 assets/icons/relay.svg delete mode 100644 assets/icons/report.svg delete mode 100644 assets/icons/resize-corner.svg delete mode 100644 assets/icons/server.svg delete mode 100644 assets/icons/signal.svg create mode 100644 assets/icons/zoom.svg diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 8c89a97..56a3127 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -13,7 +13,7 @@ jobs: build: strategy: matrix: - os: [ubuntu-latest, macos-latest, windows-latest] + os: [ubuntu-latest] rustup: [stable] runs-on: ${{ matrix.os }} diff --git a/Cargo.lock b/Cargo.lock index 7c8d9cc..a4d1561 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1179,7 +1179,7 @@ dependencies = [ [[package]] name = "collections" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ceecf82287b4f9323b4ecbfb388a147361db7aaf" +source = "git+https://github.com/zed-industries/zed#83ca31055cf3e56aa8a704ac49e1686434f4e640" dependencies = [ "indexmap", "rustc-hash 2.1.1", @@ -1607,7 +1607,7 @@ dependencies = [ [[package]] name = "derive_refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ceecf82287b4f9323b4ecbfb388a147361db7aaf" +source = "git+https://github.com/zed-industries/zed#83ca31055cf3e56aa8a704ac49e1686434f4e640" dependencies = [ "proc-macro2", "quote", @@ -2517,7 +2517,7 @@ dependencies = [ [[package]] name = "gpui" version = "0.2.2" -source = "git+https://github.com/zed-industries/zed#ceecf82287b4f9323b4ecbfb388a147361db7aaf" +source = "git+https://github.com/zed-industries/zed#83ca31055cf3e56aa8a704ac49e1686434f4e640" dependencies = [ "anyhow", "as-raw-xcb-connection", @@ -2619,7 +2619,7 @@ dependencies = [ [[package]] name = "gpui_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ceecf82287b4f9323b4ecbfb388a147361db7aaf" +source = "git+https://github.com/zed-industries/zed#83ca31055cf3e56aa8a704ac49e1686434f4e640" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -2630,7 +2630,7 @@ dependencies = [ [[package]] name = "gpui_tokio" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ceecf82287b4f9323b4ecbfb388a147361db7aaf" +source = "git+https://github.com/zed-industries/zed#83ca31055cf3e56aa8a704ac49e1686434f4e640" dependencies = [ "anyhow", "gpui", @@ -2852,7 +2852,7 @@ dependencies = [ [[package]] name = "http_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ceecf82287b4f9323b4ecbfb388a147361db7aaf" +source = "git+https://github.com/zed-industries/zed#83ca31055cf3e56aa8a704ac49e1686434f4e640" dependencies = [ "anyhow", "async-compression", @@ -2877,7 +2877,7 @@ dependencies = [ [[package]] name = "http_client_tls" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ceecf82287b4f9323b4ecbfb388a147361db7aaf" +source = "git+https://github.com/zed-industries/zed#83ca31055cf3e56aa8a704ac49e1686434f4e640" dependencies = [ "rustls", "rustls-platform-verifier", @@ -3638,7 +3638,7 @@ dependencies = [ [[package]] name = "media" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ceecf82287b4f9323b4ecbfb388a147361db7aaf" +source = "git+https://github.com/zed-industries/zed#83ca31055cf3e56aa8a704ac49e1686434f4e640" dependencies = [ "anyhow", "bindgen", @@ -4498,7 +4498,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perf" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ceecf82287b4f9323b4ecbfb388a147361db7aaf" +source = "git+https://github.com/zed-industries/zed#83ca31055cf3e56aa8a704ac49e1686434f4e640" dependencies = [ "collections", "serde", @@ -5133,7 +5133,7 @@ dependencies = [ [[package]] name = "refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ceecf82287b4f9323b4ecbfb388a147361db7aaf" +source = "git+https://github.com/zed-industries/zed#83ca31055cf3e56aa8a704ac49e1686434f4e640" dependencies = [ "derive_refineable", ] @@ -5232,7 +5232,7 @@ dependencies = [ [[package]] name = "reqwest_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ceecf82287b4f9323b4ecbfb388a147361db7aaf" +source = "git+https://github.com/zed-industries/zed#83ca31055cf3e56aa8a704ac49e1686434f4e640" dependencies = [ "anyhow", "bytes", @@ -5286,7 +5286,7 @@ dependencies = [ [[package]] name = "rope" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ceecf82287b4f9323b4ecbfb388a147361db7aaf" +source = "git+https://github.com/zed-industries/zed#83ca31055cf3e56aa8a704ac49e1686434f4e640" dependencies = [ "arrayvec", "log", @@ -5565,7 +5565,7 @@ dependencies = [ [[package]] name = "scheduler" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ceecf82287b4f9323b4ecbfb388a147361db7aaf" +source = "git+https://github.com/zed-industries/zed#83ca31055cf3e56aa8a704ac49e1686434f4e640" dependencies = [ "async-task", "backtrace", @@ -6155,7 +6155,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sum_tree" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ceecf82287b4f9323b4ecbfb388a147361db7aaf" +source = "git+https://github.com/zed-industries/zed#83ca31055cf3e56aa8a704ac49e1686434f4e640" dependencies = [ "arrayvec", "log", @@ -7113,7 +7113,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "util" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ceecf82287b4f9323b4ecbfb388a147361db7aaf" +source = "git+https://github.com/zed-industries/zed#83ca31055cf3e56aa8a704ac49e1686434f4e640" dependencies = [ "anyhow", "async-fs", @@ -7149,7 +7149,7 @@ dependencies = [ [[package]] name = "util_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ceecf82287b4f9323b4ecbfb388a147361db7aaf" +source = "git+https://github.com/zed-industries/zed#83ca31055cf3e56aa8a704ac49e1686434f4e640" dependencies = [ "perf", "quote", @@ -8625,7 +8625,7 @@ dependencies = [ [[package]] name = "zlog" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ceecf82287b4f9323b4ecbfb388a147361db7aaf" +source = "git+https://github.com/zed-industries/zed#83ca31055cf3e56aa8a704ac49e1686434f4e640" dependencies = [ "anyhow", "chrono", @@ -8642,7 +8642,7 @@ checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea" [[package]] name = "ztracing" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ceecf82287b4f9323b4ecbfb388a147361db7aaf" +source = "git+https://github.com/zed-industries/zed#83ca31055cf3e56aa8a704ac49e1686434f4e640" dependencies = [ "tracing", "tracing-subscriber", @@ -8653,7 +8653,7 @@ dependencies = [ [[package]] name = "ztracing_macro" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ceecf82287b4f9323b4ecbfb388a147361db7aaf" +source = "git+https://github.com/zed-industries/zed#83ca31055cf3e56aa8a704ac49e1686434f4e640" [[package]] name = "zune-core" diff --git a/assets/icons/arrow-left.svg b/assets/icons/arrow-left.svg index d0b5d88..ac3e659 100644 --- a/assets/icons/arrow-left.svg +++ b/assets/icons/arrow-left.svg @@ -1,3 +1,16 @@ - - + + + diff --git a/assets/icons/arrow-right.svg b/assets/icons/arrow-right.svg index 05ef2da..8aa7e0d 100644 --- a/assets/icons/arrow-right.svg +++ b/assets/icons/arrow-right.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/icons/arrows-in.svg b/assets/icons/arrows-in.svg deleted file mode 100644 index 46904e4..0000000 --- a/assets/icons/arrows-in.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/boom.svg b/assets/icons/boom.svg new file mode 100644 index 0000000..3f18d96 --- /dev/null +++ b/assets/icons/boom.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/caret-down-fill.svg b/assets/icons/caret-down-fill.svg deleted file mode 100644 index 48d34e0..0000000 --- a/assets/icons/caret-down-fill.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/caret-down.svg b/assets/icons/caret-down.svg index ca53e93..a5717ec 100644 --- a/assets/icons/caret-down.svg +++ b/assets/icons/caret-down.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/icons/caret-right.svg b/assets/icons/caret-right.svg index f86630d..1d0cc50 100644 --- a/assets/icons/caret-right.svg +++ b/assets/icons/caret-right.svg @@ -1 +1,3 @@ - + + + diff --git a/assets/icons/caret-up.svg b/assets/icons/caret-up.svg index 09e8ccd..6ffbd8b 100644 --- a/assets/icons/caret-up.svg +++ b/assets/icons/caret-up.svg @@ -1 +1,3 @@ - + + + diff --git a/assets/icons/check-circle-fill.svg b/assets/icons/check-circle-fill.svg deleted file mode 100644 index e776f4f..0000000 --- a/assets/icons/check-circle-fill.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/check-circle.svg b/assets/icons/check-circle.svg index 4889d9c..74e45c3 100644 --- a/assets/icons/check-circle.svg +++ b/assets/icons/check-circle.svg @@ -1 +1,3 @@ - + + + diff --git a/assets/icons/check.svg b/assets/icons/check.svg index 5b34ba7..5105a0d 100644 --- a/assets/icons/check.svg +++ b/assets/icons/check.svg @@ -1 +1,3 @@ - + + + diff --git a/assets/icons/close-circle-fill.svg b/assets/icons/close-circle-fill.svg index 9e7b90f..27c42e3 100644 --- a/assets/icons/close-circle-fill.svg +++ b/assets/icons/close-circle-fill.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/icons/close-circle.svg b/assets/icons/close-circle.svg index 9e7b90f..b622fb0 100644 --- a/assets/icons/close-circle.svg +++ b/assets/icons/close-circle.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/icons/close.svg b/assets/icons/close.svg index 55af35f..debd9cb 100644 --- a/assets/icons/close.svg +++ b/assets/icons/close.svg @@ -1 +1,3 @@ - + + + diff --git a/assets/icons/copy.svg b/assets/icons/copy.svg index 5af5438..2daf6a6 100644 --- a/assets/icons/copy.svg +++ b/assets/icons/copy.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/icons/door.svg b/assets/icons/door.svg new file mode 100644 index 0000000..df03a31 --- /dev/null +++ b/assets/icons/door.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/edit.svg b/assets/icons/edit.svg deleted file mode 100644 index 0bcd4e0..0000000 --- a/assets/icons/edit.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/ellipsis.svg b/assets/icons/ellipsis.svg index f3113af..2ab4cc7 100644 --- a/assets/icons/ellipsis.svg +++ b/assets/icons/ellipsis.svg @@ -1,4 +1,3 @@ - - - + + diff --git a/assets/icons/emoji-fill.svg b/assets/icons/emoji-fill.svg deleted file mode 100644 index 974ccf4..0000000 --- a/assets/icons/emoji-fill.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/emoji.svg b/assets/icons/emoji.svg new file mode 100644 index 0000000..fd52a5f --- /dev/null +++ b/assets/icons/emoji.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/encryption.svg b/assets/icons/encryption.svg deleted file mode 100644 index 4f23a2d..0000000 --- a/assets/icons/encryption.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/eye.svg b/assets/icons/eye.svg new file mode 100644 index 0000000..907673c --- /dev/null +++ b/assets/icons/eye.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/group.svg b/assets/icons/group.svg deleted file mode 100644 index 7f94e3c..0000000 --- a/assets/icons/group.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/info.svg b/assets/icons/info.svg index 1b3641a..48994c5 100644 --- a/assets/icons/info.svg +++ b/assets/icons/info.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/icons/link.svg b/assets/icons/link.svg new file mode 100644 index 0000000..88ae017 --- /dev/null +++ b/assets/icons/link.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/logout.svg b/assets/icons/logout.svg deleted file mode 100644 index e7beade..0000000 --- a/assets/icons/logout.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/minimize.svg b/assets/icons/minimize.svg deleted file mode 100644 index 67009bf..0000000 --- a/assets/icons/minimize.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/moon.svg b/assets/icons/moon.svg index c161221..1684e5f 100644 --- a/assets/icons/moon.svg +++ b/assets/icons/moon.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/icons/open-url.svg b/assets/icons/open-url.svg deleted file mode 100644 index 837e1e6..0000000 --- a/assets/icons/open-url.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/panel-left-open.svg b/assets/icons/panel-left-open.svg deleted file mode 100644 index bd9af3e..0000000 --- a/assets/icons/panel-left-open.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/panel-left.svg b/assets/icons/panel-left.svg deleted file mode 100644 index def688f..0000000 --- a/assets/icons/panel-left.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/panel-right-open.svg b/assets/icons/panel-right-open.svg deleted file mode 100644 index 0cf3c3c..0000000 --- a/assets/icons/panel-right-open.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/panel-right.svg b/assets/icons/panel-right.svg deleted file mode 100644 index f7278a5..0000000 --- a/assets/icons/panel-right.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/plus-circle-fill.svg b/assets/icons/plus-circle-fill.svg deleted file mode 100644 index 848c240..0000000 --- a/assets/icons/plus-circle-fill.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/icons/plus-circle.svg b/assets/icons/plus-circle.svg new file mode 100644 index 0000000..a615ad7 --- /dev/null +++ b/assets/icons/plus-circle.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/plus-fill.svg b/assets/icons/plus-fill.svg deleted file mode 100644 index 1ba3086..0000000 --- a/assets/icons/plus-fill.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/plus.svg b/assets/icons/plus.svg index 8cf89a0..f9e4c8f 100644 --- a/assets/icons/plus.svg +++ b/assets/icons/plus.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/icons/refresh.svg b/assets/icons/refresh.svg deleted file mode 100644 index ed200e0..0000000 --- a/assets/icons/refresh.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/relay.svg b/assets/icons/relay.svg new file mode 100644 index 0000000..cd47109 --- /dev/null +++ b/assets/icons/relay.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/reply.svg b/assets/icons/reply.svg index 53da0cb..8a36e4c 100644 --- a/assets/icons/reply.svg +++ b/assets/icons/reply.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/icons/report.svg b/assets/icons/report.svg deleted file mode 100644 index 07c1403..0000000 --- a/assets/icons/report.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/resize-corner.svg b/assets/icons/resize-corner.svg deleted file mode 100644 index 31065be..0000000 --- a/assets/icons/resize-corner.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/assets/icons/search.svg b/assets/icons/search.svg index 5207a79..c519f15 100644 --- a/assets/icons/search.svg +++ b/assets/icons/search.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/icons/server.svg b/assets/icons/server.svg deleted file mode 100644 index 841911c..0000000 --- a/assets/icons/server.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/settings.svg b/assets/icons/settings.svg index 059f4a8..01426e5 100644 --- a/assets/icons/settings.svg +++ b/assets/icons/settings.svg @@ -1,4 +1,3 @@ - - - + + diff --git a/assets/icons/signal.svg b/assets/icons/signal.svg deleted file mode 100644 index 5c46c01..0000000 --- a/assets/icons/signal.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/icons/sun.svg b/assets/icons/sun.svg index de6bb8b..07e5909 100644 --- a/assets/icons/sun.svg +++ b/assets/icons/sun.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/icons/upload.svg b/assets/icons/upload.svg index 448cd21..ffa5b34 100644 --- a/assets/icons/upload.svg +++ b/assets/icons/upload.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/icons/warning.svg b/assets/icons/warning.svg index 8ac94eb..0854261 100644 --- a/assets/icons/warning.svg +++ b/assets/icons/warning.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/icons/zoom.svg b/assets/icons/zoom.svg new file mode 100644 index 0000000..663232e --- /dev/null +++ b/assets/icons/zoom.svg @@ -0,0 +1,58 @@ + + + + + + + + + + diff --git a/crates/chat_ui/src/lib.rs b/crates/chat_ui/src/lib.rs index 109182f..7b5b90c 100644 --- a/crates/chat_ui/src/lib.rs +++ b/crates/chat_ui/src/lib.rs @@ -1199,7 +1199,7 @@ impl Render for ChatPanel { .child( EmojiPicker::new() .target(self.input.downgrade()) - .icon(IconName::EmojiFill) + .icon(IconName::Emoji) .large(), ), ) diff --git a/crates/coop/src/chatspace.rs b/crates/coop/src/chatspace.rs index 4616486..e290321 100644 --- a/crates/coop/src/chatspace.rs +++ b/crates/coop/src/chatspace.rs @@ -545,15 +545,6 @@ impl ChatSpace { let persons = PersonRegistry::global(cx); let profile = persons.read(cx).get(&public_key, cx); - let keystore = KeyStore::global(cx); - let is_using_file_keystore = keystore.read(cx).is_using_file_keystore(); - - let keyring_label = if is_using_file_keystore { - SharedString::from("Disabled") - } else { - SharedString::from("Enabled") - }; - this.child( Button::new("user") .small() @@ -563,29 +554,17 @@ impl ChatSpace { .child(Avatar::new(profile.avatar()).size(rems(1.45))) .popup_menu(move |this, _window, _cx| { this.label(profile.name()) - .menu_with_icon( - "Profile", - IconName::EmojiFill, - Box::new(ViewProfile), - ) + .menu_with_icon("Profile", IconName::Emoji, Box::new(ViewProfile)) .menu_with_icon( "Messaging Relays", - IconName::Server, + IconName::Relay, Box::new(ViewRelays), ) .separator() - .label(SharedString::from("Keyring Service")) - .menu_with_icon_and_disabled( - keyring_label.clone(), - IconName::Encryption, - Box::new(KeyringPopup), - !is_using_file_keystore, - ) - .separator() .menu_with_icon("Dark Mode", IconName::Sun, Box::new(DarkMode)) .menu_with_icon("Themes", IconName::Moon, Box::new(Themes)) .menu_with_icon("Settings", IconName::Settings, Box::new(Settings)) - .menu_with_icon("Sign Out", IconName::Logout, Box::new(Logout)) + .menu_with_icon("Sign Out", IconName::Door, Box::new(Logout)) }), ) }) diff --git a/crates/coop/src/new_identity/backup.rs b/crates/coop/src/new_identity/backup.rs index 36858db..f5a52b8 100644 --- a/crates/coop/src/new_identity/backup.rs +++ b/crates/coop/src/new_identity/backup.rs @@ -151,7 +151,7 @@ impl Render for Backup { Button::new("copy-pubkey") .icon({ if self.copied { - IconName::CheckCircleFill + IconName::CheckCircle } else { IconName::Copy } @@ -187,7 +187,7 @@ impl Render for Backup { Button::new("copy-secret") .icon({ if self.copied { - IconName::CheckCircleFill + IconName::CheckCircle } else { IconName::Copy } diff --git a/crates/coop/src/new_identity/mod.rs b/crates/coop/src/new_identity/mod.rs index ab7daa8..8a1c3aa 100644 --- a/crates/coop/src/new_identity/mod.rs +++ b/crates/coop/src/new_identity/mod.rs @@ -311,7 +311,7 @@ impl Render for NewAccount { .child(Avatar::new(avatar).size(rems(4.25))) .child( Button::new("upload") - .icon(IconName::PlusCircleFill) + .icon(IconName::PlusCircle) .label("Add an avatar") .xsmall() .ghost() diff --git a/crates/coop/src/sidebar/mod.rs b/crates/coop/src/sidebar/mod.rs index bfb823b..d8f4ef4 100644 --- a/crates/coop/src/sidebar/mod.rs +++ b/crates/coop/src/sidebar/mod.rs @@ -511,7 +511,7 @@ impl Sidebar { .gap_1() .font_semibold() .child( - Icon::new(IconName::Signal) + Icon::new(IconName::Relay) .small() .text_color(cx.theme().danger_active) .when(connected, |this| { diff --git a/crates/coop/src/user/mod.rs b/crates/coop/src/user/mod.rs index cfee59a..8bdabc2 100644 --- a/crates/coop/src/user/mod.rs +++ b/crates/coop/src/user/mod.rs @@ -366,7 +366,7 @@ impl Render for UserProfile { Button::new("copy") .icon({ if self.copied { - IconName::CheckCircleFill + IconName::CheckCircle } else { IconName::Copy } diff --git a/crates/coop/src/user/viewer.rs b/crates/coop/src/user/viewer.rs index e710dba..57f3c04 100644 --- a/crates/coop/src/user/viewer.rs +++ b/crates/coop/src/user/viewer.rs @@ -168,7 +168,7 @@ impl Render for ProfileViewer { .relative() .text_color(cx.theme().text_accent) .child( - Icon::new(IconName::CheckCircleFill) + Icon::new(IconName::CheckCircle) .small() .block(), ), @@ -240,7 +240,7 @@ impl Render for ProfileViewer { Button::new("copy") .icon({ if self.copied { - IconName::CheckCircleFill + IconName::CheckCircle } else { IconName::Copy } diff --git a/crates/coop/src/views/compose.rs b/crates/coop/src/views/compose.rs index cca222c..f38b00d 100644 --- a/crates/coop/src/views/compose.rs +++ b/crates/coop/src/views/compose.rs @@ -386,7 +386,7 @@ impl Compose { ) .when(contact.selected, |this| { this.child( - Icon::new(IconName::CheckCircleFill) + Icon::new(IconName::CheckCircle) .small() .text_color(cx.theme().text_accent), ) @@ -459,7 +459,7 @@ impl Render for Compose { .disabled(loading) .suffix( Button::new("add") - .icon(IconName::PlusCircleFill) + .icon(IconName::PlusCircle) .transparent() .small() .disabled(loading) diff --git a/crates/coop/src/views/screening.rs b/crates/coop/src/views/screening.rs index bc0705f..8ab5443 100644 --- a/crates/coop/src/views/screening.rs +++ b/crates/coop/src/views/screening.rs @@ -278,7 +278,7 @@ impl Render for Screening { .child( Button::new("report") .tooltip("Report as a scam or impostor") - .icon(IconName::Report) + .icon(IconName::Boom) .danger() .rounded() .on_click(cx.listener(move |this, _e, window, cx| { @@ -440,7 +440,7 @@ fn status_badge(status: Option, cx: &App) -> Div { .flex_shrink_0() .map(|this| { if let Some(status) = status { - this.child(Icon::new(IconName::CheckCircleFill).small().text_color({ + this.child(Icon::new(IconName::CheckCircle).small().text_color({ if status { cx.theme().icon_accent } else { diff --git a/crates/coop/src/views/setup_relay.rs b/crates/coop/src/views/setup_relay.rs index cbc2809..a79b969 100644 --- a/crates/coop/src/views/setup_relay.rs +++ b/crates/coop/src/views/setup_relay.rs @@ -296,7 +296,7 @@ impl Render for SetupRelay { .child(TextInput::new(&self.input).small()) .child( Button::new("add") - .icon(IconName::PlusFill) + .icon(IconName::Plus) .label("Add") .ghost() .on_click(cx.listener(move |this, _, window, cx| { diff --git a/crates/ui/src/dock_area/tab_panel.rs b/crates/ui/src/dock_area/tab_panel.rs index f92b7d3..b91e635 100644 --- a/crates/ui/src/dock_area/tab_panel.rs +++ b/crates/ui/src/dock_area/tab_panel.rs @@ -423,7 +423,7 @@ impl TabPanel { .when(self.is_zoomed, |this| { this.child( Button::new("zoom") - .icon(IconName::ArrowIn) + .icon(IconName::Zoom) .small() .ghost() .tooltip("Zoom Out") diff --git a/crates/ui/src/icon.rs b/crates/ui/src/icon.rs index d3dd4de..4dcbd3d 100644 --- a/crates/ui/src/icon.rs +++ b/crates/ui/src/icon.rs @@ -9,127 +9,79 @@ use crate::{Sizable, Size}; #[derive(IntoElement, Clone)] pub enum IconName { - ArrowIn, - ArrowDown, ArrowLeft, ArrowRight, - ArrowUp, - CaretUp, + Boom, CaretDown, - CaretDownFill, CaretRight, + CaretUp, Check, CheckCircle, - CheckCircleFill, Close, CloseCircle, CloseCircleFill, Copy, - Edit, + Door, Ellipsis, - Encryption, + Emoji, Eye, - EyeOff, - EmojiFill, Info, + Link, Loader, - Logout, Moon, - PanelBottom, - PanelBottomOpen, - PanelLeft, - PanelLeftClose, - PanelLeftOpen, - PanelRight, - PanelRightClose, - PanelRightOpen, Plus, - PlusFill, - PlusCircleFill, - Group, - ResizeCorner, + PlusCircle, + Relay, Reply, - Report, - Refresh, - Signal, Search, Settings, - Server, - SortAscending, - SortDescending, Sun, - ThumbsDown, - ThumbsUp, Upload, - OpenUrl, Warning, WindowClose, WindowMaximize, WindowMinimize, WindowRestore, + Zoom, } impl IconName { pub fn path(self) -> SharedString { match self { - Self::ArrowIn => "icons/arrows-in.svg", - Self::ArrowDown => "icons/arrow-down.svg", Self::ArrowLeft => "icons/arrow-left.svg", Self::ArrowRight => "icons/arrow-right.svg", - Self::ArrowUp => "icons/arrow-up.svg", + Self::Boom => "icons/boom.svg", + Self::CaretDown => "icons/caret-down.svg", Self::CaretRight => "icons/caret-right.svg", Self::CaretUp => "icons/caret-up.svg", - Self::CaretDown => "icons/caret-down.svg", - Self::CaretDownFill => "icons/caret-down-fill.svg", Self::Check => "icons/check.svg", Self::CheckCircle => "icons/check-circle.svg", - Self::CheckCircleFill => "icons/check-circle-fill.svg", Self::Close => "icons/close.svg", Self::CloseCircle => "icons/close-circle.svg", Self::CloseCircleFill => "icons/close-circle-fill.svg", Self::Copy => "icons/copy.svg", - Self::Edit => "icons/edit.svg", + Self::Door => "icons/door.svg", Self::Ellipsis => "icons/ellipsis.svg", + Self::Emoji => "icons/emoji.svg", Self::Eye => "icons/eye.svg", - Self::Encryption => "icons/encryption.svg", - Self::EmojiFill => "icons/emoji-fill.svg", - Self::EyeOff => "icons/eye-off.svg", Self::Info => "icons/info.svg", + Self::Link => "icons/link.svg", Self::Loader => "icons/loader.svg", - Self::Logout => "icons/logout.svg", Self::Moon => "icons/moon.svg", - Self::PanelBottom => "icons/panel-bottom.svg", - Self::PanelBottomOpen => "icons/panel-bottom-open.svg", - Self::PanelLeft => "icons/panel-left.svg", - Self::PanelLeftClose => "icons/panel-left-close.svg", - Self::PanelLeftOpen => "icons/panel-left-open.svg", - Self::PanelRight => "icons/panel-right.svg", - Self::PanelRightClose => "icons/panel-right-close.svg", - Self::PanelRightOpen => "icons/panel-right-open.svg", Self::Plus => "icons/plus.svg", - Self::PlusFill => "icons/plus-fill.svg", - Self::PlusCircleFill => "icons/plus-circle-fill.svg", - Self::Group => "icons/group.svg", - Self::ResizeCorner => "icons/resize-corner.svg", + Self::PlusCircle => "icons/plus-circle.svg", + Self::Relay => "icons/relay.svg", Self::Reply => "icons/reply.svg", - Self::Report => "icons/report.svg", - Self::Refresh => "icons/refresh.svg", - Self::Signal => "icons/signal.svg", Self::Search => "icons/search.svg", Self::Settings => "icons/settings.svg", - Self::Server => "icons/server.svg", - Self::SortAscending => "icons/sort-ascending.svg", - Self::SortDescending => "icons/sort-descending.svg", Self::Sun => "icons/sun.svg", - Self::ThumbsDown => "icons/thumbs-down.svg", - Self::ThumbsUp => "icons/thumbs-up.svg", Self::Upload => "icons/upload.svg", - Self::OpenUrl => "icons/open-url.svg", Self::Warning => "icons/warning.svg", Self::WindowClose => "icons/window-close.svg", Self::WindowMaximize => "icons/window-maximize.svg", Self::WindowMinimize => "icons/window-minimize.svg", Self::WindowRestore => "icons/window-restore.svg", + Self::Zoom => "icons/zoom.svg", } .into() } diff --git a/crates/ui/src/menu/popup_menu.rs b/crates/ui/src/menu/popup_menu.rs index 7e08e52..cf3105e 100644 --- a/crates/ui/src/menu/popup_menu.rs +++ b/crates/ui/src/menu/popup_menu.rs @@ -1015,7 +1015,7 @@ impl PopupMenu { .gap_1p5() .child(label.clone()) .child( - Icon::new(IconName::OpenUrl) + Icon::new(IconName::Link) .xsmall() .text_color(cx.theme().text_muted), ), -- 2.49.1 From 81b1f2b29338ef5742189046321c50e1e68fb0f7 Mon Sep 17 00:00:00 2001 From: reya Date: Fri, 16 Jan 2026 15:29:34 +0700 Subject: [PATCH 03/30] refactor root component --- crates/chat_ui/src/lib.rs | 4 +- crates/coop/src/chatspace.rs | 2 +- crates/coop/src/login/mod.rs | 2 +- crates/coop/src/main.rs | 3 +- crates/coop/src/new_identity/mod.rs | 2 +- crates/coop/src/sidebar/list_item.rs | 2 +- crates/coop/src/sidebar/mod.rs | 2 +- crates/coop/src/user/mod.rs | 2 +- crates/coop/src/views/compose.rs | 2 +- crates/coop/src/views/onboarding.rs | 2 +- crates/coop/src/views/screening.rs | 2 +- crates/coop/src/views/setup_relay.rs | 2 +- crates/coop/src/views/startup.rs | 2 +- crates/relay_auth/src/lib.rs | 4 +- crates/theme/src/lib.rs | 3 + crates/ui/src/lib.rs | 6 +- crates/ui/src/modal.rs | 12 +- crates/ui/src/notification.rs | 2 +- crates/ui/src/root.rs | 477 +++++++++++++++++---------- crates/ui/src/window_border.rs | 204 ------------ crates/ui/src/window_ext.rs | 120 +++++++ 21 files changed, 445 insertions(+), 412 deletions(-) delete mode 100644 crates/ui/src/window_border.rs create mode 100644 crates/ui/src/window_ext.rs diff --git a/crates/chat_ui/src/lib.rs b/crates/chat_ui/src/lib.rs index 7b5b90c..9bbd428 100644 --- a/crates/chat_ui/src/lib.rs +++ b/crates/chat_ui/src/lib.rs @@ -30,8 +30,8 @@ use ui::input::{InputEvent, InputState, TextInput}; use ui::notification::Notification; use ui::popup_menu::PopupMenuExt; use ui::{ - h_flex, v_flex, ContextModal, Disableable, Icon, IconName, InteractiveElementExt, Sizable, - StyledExt, + h_flex, v_flex, Disableable, Icon, IconName, InteractiveElementExt, Sizable, StyledExt, + WindowExtension, }; use crate::emoji::EmojiPicker; diff --git a/crates/coop/src/chatspace.rs b/crates/coop/src/chatspace.rs index e290321..6f85925 100644 --- a/crates/coop/src/chatspace.rs +++ b/crates/coop/src/chatspace.rs @@ -25,7 +25,7 @@ use ui::dock_area::panel::PanelView; use ui::dock_area::{ClosePanel, DockArea, DockItem}; use ui::modal::ModalButtonProps; use ui::popup_menu::PopupMenuExt; -use ui::{h_flex, v_flex, ContextModal, IconName, Root, Sizable, StyledExt}; +use ui::{h_flex, v_flex, IconName, Root, Sizable, StyledExt, WindowExtension}; use crate::actions::{ reset, DarkMode, KeyringPopup, Logout, Settings, Themes, ViewProfile, ViewRelays, diff --git a/crates/coop/src/login/mod.rs b/crates/coop/src/login/mod.rs index 324b167..b93ad3c 100644 --- a/crates/coop/src/login/mod.rs +++ b/crates/coop/src/login/mod.rs @@ -16,7 +16,7 @@ use ui::button::{Button, ButtonVariants}; use ui::dock_area::panel::{Panel, PanelEvent}; use ui::input::{InputEvent, InputState, TextInput}; use ui::notification::Notification; -use ui::{v_flex, ContextModal, Disableable, StyledExt}; +use ui::{v_flex, Disableable, StyledExt, WindowExtension}; use crate::actions::CoopAuthUrlHandler; diff --git a/crates/coop/src/main.rs b/crates/coop/src/main.rs index a1fb4de..3777c3a 100644 --- a/crates/coop/src/main.rs +++ b/crates/coop/src/main.rs @@ -4,7 +4,7 @@ use assets::Assets; use common::{APP_ID, CLIENT_NAME}; use gpui::{ point, px, size, AppContext, Application, Bounds, KeyBinding, Menu, MenuItem, SharedString, - TitlebarOptions, WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, + Size, TitlebarOptions, WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, WindowOptions, }; use ui::Root; @@ -58,6 +58,7 @@ fn main() { window_background: WindowBackgroundAppearance::Opaque, window_decorations: Some(WindowDecorations::Client), window_bounds: Some(WindowBounds::Windowed(bounds)), + window_min_size: Some(Size::new(px(640.), px(480.))), kind: WindowKind::Normal, app_id: Some(APP_ID.to_owned()), titlebar: Some(TitlebarOptions { diff --git a/crates/coop/src/new_identity/mod.rs b/crates/coop/src/new_identity/mod.rs index 8a1c3aa..89d7611 100644 --- a/crates/coop/src/new_identity/mod.rs +++ b/crates/coop/src/new_identity/mod.rs @@ -15,7 +15,7 @@ use ui::button::{Button, ButtonVariants}; use ui::dock_area::panel::{Panel, PanelEvent}; use ui::input::{InputState, TextInput}; use ui::modal::ModalButtonProps; -use ui::{divider, v_flex, ContextModal, Disableable, IconName, Sizable}; +use ui::{divider, v_flex, Disableable, IconName, Sizable, WindowExtension}; mod backup; diff --git a/crates/coop/src/sidebar/list_item.rs b/crates/coop/src/sidebar/list_item.rs index a018f38..b68369a 100644 --- a/crates/coop/src/sidebar/list_item.rs +++ b/crates/coop/src/sidebar/list_item.rs @@ -14,7 +14,7 @@ use ui::avatar::Avatar; use ui::context_menu::ContextMenuExt; use ui::modal::ModalButtonProps; use ui::skeleton::Skeleton; -use ui::{h_flex, ContextModal, StyledExt}; +use ui::{h_flex, StyledExt, WindowExtension}; use crate::views::screening; diff --git a/crates/coop/src/sidebar/mod.rs b/crates/coop/src/sidebar/mod.rs index d8f4ef4..a64842c 100644 --- a/crates/coop/src/sidebar/mod.rs +++ b/crates/coop/src/sidebar/mod.rs @@ -20,7 +20,7 @@ use ui::button::{Button, ButtonVariants}; use ui::dock_area::panel::{Panel, PanelEvent}; use ui::input::{InputEvent, InputState, TextInput}; use ui::popup_menu::PopupMenuExt; -use ui::{h_flex, v_flex, ContextModal, Icon, IconName, Selectable, Sizable, StyledExt}; +use ui::{h_flex, v_flex, Icon, IconName, Selectable, Sizable, StyledExt, WindowExtension}; use crate::actions::{RelayStatus, Reload}; diff --git a/crates/coop/src/user/mod.rs b/crates/coop/src/user/mod.rs index 8bdabc2..54b1ea1 100644 --- a/crates/coop/src/user/mod.rs +++ b/crates/coop/src/user/mod.rs @@ -18,7 +18,7 @@ use state::NostrRegistry; use theme::ActiveTheme; use ui::button::{Button, ButtonVariants}; use ui::input::{InputState, TextInput}; -use ui::{h_flex, v_flex, ContextModal, Disableable, IconName, Sizable, StyledExt}; +use ui::{h_flex, v_flex, Disableable, IconName, Sizable, StyledExt, WindowExtension}; pub mod viewer; diff --git a/crates/coop/src/views/compose.rs b/crates/coop/src/views/compose.rs index f38b00d..4fcec5a 100644 --- a/crates/coop/src/views/compose.rs +++ b/crates/coop/src/views/compose.rs @@ -21,7 +21,7 @@ use ui::button::{Button, ButtonVariants}; use ui::input::{InputEvent, InputState, TextInput}; use ui::modal::ModalButtonProps; use ui::notification::Notification; -use ui::{h_flex, v_flex, ContextModal, Disableable, Icon, IconName, Sizable, StyledExt}; +use ui::{h_flex, v_flex, Disableable, Icon, IconName, Sizable, StyledExt, WindowExtension}; pub fn compose_button() -> impl IntoElement { div().child( diff --git a/crates/coop/src/views/onboarding.rs b/crates/coop/src/views/onboarding.rs index d7d9c8f..ba29e34 100644 --- a/crates/coop/src/views/onboarding.rs +++ b/crates/coop/src/views/onboarding.rs @@ -16,7 +16,7 @@ use theme::ActiveTheme; use ui::button::{Button, ButtonVariants}; use ui::dock_area::panel::{Panel, PanelEvent}; use ui::notification::Notification; -use ui::{divider, h_flex, v_flex, ContextModal, Icon, IconName, Sizable, StyledExt}; +use ui::{divider, h_flex, v_flex, Icon, IconName, Sizable, StyledExt, WindowExtension}; use crate::chatspace::{self}; diff --git a/crates/coop/src/views/screening.rs b/crates/coop/src/views/screening.rs index 8ab5443..f61a949 100644 --- a/crates/coop/src/views/screening.rs +++ b/crates/coop/src/views/screening.rs @@ -15,7 +15,7 @@ use theme::ActiveTheme; use ui::avatar::Avatar; use ui::button::{Button, ButtonVariants}; use ui::indicator::Indicator; -use ui::{h_flex, v_flex, ContextModal, Icon, IconName, Sizable, StyledExt}; +use ui::{h_flex, v_flex, Icon, IconName, Sizable, StyledExt, WindowExtension}; pub fn init(public_key: PublicKey, window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Screening::new(public_key, window, cx)) diff --git a/crates/coop/src/views/setup_relay.rs b/crates/coop/src/views/setup_relay.rs index a79b969..e237b80 100644 --- a/crates/coop/src/views/setup_relay.rs +++ b/crates/coop/src/views/setup_relay.rs @@ -14,7 +14,7 @@ use state::NostrRegistry; use theme::ActiveTheme; use ui::button::{Button, ButtonVariants}; use ui::input::{InputEvent, InputState, TextInput}; -use ui::{h_flex, v_flex, ContextModal, IconName, Sizable}; +use ui::{h_flex, v_flex, IconName, Sizable, WindowExtension}; pub fn init(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| SetupRelay::new(window, cx)) diff --git a/crates/coop/src/views/startup.rs b/crates/coop/src/views/startup.rs index a79aa1c..4a77d82 100644 --- a/crates/coop/src/views/startup.rs +++ b/crates/coop/src/views/startup.rs @@ -18,7 +18,7 @@ use ui::avatar::Avatar; use ui::button::{Button, ButtonVariants}; use ui::dock_area::panel::{Panel, PanelEvent}; use ui::indicator::Indicator; -use ui::{h_flex, v_flex, ContextModal, Sizable, StyledExt}; +use ui::{h_flex, v_flex, Sizable, StyledExt, WindowExtension}; use crate::actions::{reset, CoopAuthUrlHandler}; diff --git a/crates/relay_auth/src/lib.rs b/crates/relay_auth/src/lib.rs index b753261..99aa2f1 100644 --- a/crates/relay_auth/src/lib.rs +++ b/crates/relay_auth/src/lib.rs @@ -16,7 +16,7 @@ use state::{tracker, NostrRegistry}; use theme::ActiveTheme; use ui::button::{Button, ButtonVariants}; use ui::notification::Notification; -use ui::{v_flex, ContextModal, Disableable, IconName, Sizable}; +use ui::{v_flex, Disableable, IconName, Sizable, WindowExtension}; const AUTH_MESSAGE: &str = "Approve the authentication request to allow Coop to continue sending or receiving events."; @@ -243,7 +243,7 @@ impl RelayAuth { Ok(_) => { this.update_in(cx, |this, window, cx| { // Clear the current notification - window.clear_notification_by_id(SharedString::from(&challenge), cx); + window.clear_notification(challenge, cx); // Push a new notification window.push_notification(format!("{url} has been authenticated"), cx); diff --git a/crates/theme/src/lib.rs b/crates/theme/src/lib.rs index 14a06c3..66b65b9 100644 --- a/crates/theme/src/lib.rs +++ b/crates/theme/src/lib.rs @@ -21,6 +21,9 @@ pub const CLIENT_SIDE_DECORATION_ROUNDING: Pixels = px(10.0); /// Defines window shadow size for platforms that use client side decorations. pub const CLIENT_SIDE_DECORATION_SHADOW: Pixels = px(10.0); +/// Defines window border size for platforms that use client side decorations. +pub const CLIENT_SIDE_DECORATION_BORDER: Pixels = px(1.0); + pub fn init(cx: &mut App) { registry::init(cx); diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index 5cbbe7e..5875b15 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -3,9 +3,9 @@ pub use focusable::FocusableCycle; pub use icon::*; pub use kbd::*; pub use menu::{context_menu, popup_menu}; -pub use root::{ContextModal, Root}; +pub use root::Root; pub use styled::*; -pub use window_border::{window_border, WindowBorder}; +pub use window_ext::*; pub use crate::Disableable; @@ -38,7 +38,7 @@ mod icon; mod kbd; mod root; mod styled; -mod window_border; +mod window_ext; /// Initialize the UI module. /// diff --git a/crates/ui/src/modal.rs b/crates/ui/src/modal.rs index 6678b88..226dcee 100644 --- a/crates/ui/src/modal.rs +++ b/crates/ui/src/modal.rs @@ -13,7 +13,7 @@ use theme::ActiveTheme; use crate::actions::{Cancel, Confirm}; use crate::animation::cubic_bezier; use crate::button::{Button, ButtonCustomVariant, ButtonVariant, ButtonVariants as _}; -use crate::{h_flex, v_flex, ContextModal, IconName, Root, Sizable, StyledExt}; +use crate::{h_flex, v_flex, IconName, Root, Sizable, StyledExt, WindowExtension}; const CONTEXT: &str = "Modal"; @@ -97,9 +97,9 @@ pub struct Modal { button_props: ModalButtonProps, /// This will be change when open the modal, the focus handle is create when open the modal. - pub(crate) focus_handle: FocusHandle, - pub(crate) layer_ix: usize, - pub(crate) overlay_visible: bool, + pub focus_handle: FocusHandle, + pub layer_ix: usize, + pub overlay_visible: bool, } impl Modal { @@ -255,7 +255,7 @@ impl Modal { self } - pub(crate) fn has_overlay(&self) -> bool { + pub fn has_overlay(&self) -> bool { self.overlay } } @@ -341,7 +341,7 @@ impl RenderOnce for Modal { } }); - let window_paddings = crate::window_border::window_paddings(window, cx); + let window_paddings = crate::root::window_paddings(window, cx); let radius = (cx.theme().radius_lg * 2.).min(px(20.)); let view_size = window.viewport_size() diff --git a/crates/ui/src/notification.rs b/crates/ui/src/notification.rs index 8c8b2e4..6b60319 100644 --- a/crates/ui/src/notification.rs +++ b/crates/ui/src/notification.rs @@ -425,7 +425,7 @@ impl NotificationList { cx.notify(); } - pub(crate) fn close(&mut self, key: T, window: &mut Window, cx: &mut Context) + pub fn close(&mut self, key: T, window: &mut Window, cx: &mut Context) where T: Into, { diff --git a/crates/ui/src/root.rs b/crates/ui/src/root.rs index 3dbddbc..0998591 100644 --- a/crates/ui/src/root.rs +++ b/crates/ui/src/root.rs @@ -2,168 +2,63 @@ use std::rc::Rc; use gpui::prelude::FluentBuilder; use gpui::{ - div, AnyView, App, AppContext, Context, Decorations, Entity, FocusHandle, InteractiveElement, - IntoElement, ParentElement as _, Render, SharedString, Styled, Window, + canvas, div, point, px, AnyView, App, AppContext, Bounds, Context, CursorStyle, Decorations, + Edges, Entity, FocusHandle, HitboxBehavior, Hsla, InteractiveElement, IntoElement, MouseButton, + ParentElement as _, Pixels, Point, Render, ResizeEdge, SharedString, Size, Styled, + WeakFocusHandle, Window, +}; +use theme::{ + ActiveTheme, CLIENT_SIDE_DECORATION_BORDER, CLIENT_SIDE_DECORATION_ROUNDING, + CLIENT_SIDE_DECORATION_SHADOW, }; -use theme::{ActiveTheme, CLIENT_SIDE_DECORATION_ROUNDING}; use crate::input::InputState; use crate::modal::Modal; use crate::notification::{Notification, NotificationList}; -use crate::window_border; - -/// Extension trait for [`WindowContext`] and [`ViewContext`] to add drawer functionality. -pub trait ContextModal: Sized { - /// Opens a Modal. - fn open_modal(&mut self, cx: &mut App, build: F) - where - F: Fn(Modal, &mut Window, &mut App) -> Modal + 'static; - - /// Return true, if there is an active Modal. - fn has_active_modal(&mut self, cx: &mut App) -> bool; - - /// Closes the last active Modal. - fn close_modal(&mut self, cx: &mut App); - - /// Closes all active Modals. - fn close_all_modals(&mut self, cx: &mut App); - - /// Returns number of notifications. - fn notifications(&mut self, cx: &mut App) -> Rc>>; - - /// Pushes a notification to the notification list. - fn push_notification(&mut self, note: impl Into, cx: &mut App); - - /// Clears a notification by its ID. - fn clear_notification_by_id(&mut self, id: SharedString, cx: &mut App); - - /// Clear all notifications - fn clear_notifications(&mut self, cx: &mut App); - - /// Return current focused Input entity. - fn focused_input(&mut self, cx: &mut App) -> Option>; - - /// Returns true if there is a focused Input entity. - fn has_focused_input(&mut self, cx: &mut App) -> bool; -} - -impl ContextModal for Window { - fn open_modal(&mut self, cx: &mut App, build: F) - where - F: Fn(Modal, &mut Window, &mut App) -> Modal + 'static, - { - Root::update(self, cx, move |root, window, cx| { - // Only save focus handle if there are no active modals. - // This is used to restore focus when all modals are closed. - if root.active_modals.is_empty() { - root.previous_focus_handle = window.focused(cx); - } - - let focus_handle = cx.focus_handle(); - focus_handle.focus(window, cx); - - root.active_modals.push(ActiveModal { - focus_handle, - builder: Rc::new(build), - }); - - cx.notify(); - }) - } - - fn has_active_modal(&mut self, cx: &mut App) -> bool { - !Root::read(self, cx).active_modals.is_empty() - } - - fn close_modal(&mut self, cx: &mut App) { - Root::update(self, cx, move |root, window, cx| { - root.active_modals.pop(); - - if let Some(top_modal) = root.active_modals.last() { - // Focus the next modal. - top_modal.focus_handle.focus(window, cx); - } else { - // Restore focus if there are no more modals. - root.focus_back(window, cx); - } - cx.notify(); - }) - } - - fn close_all_modals(&mut self, cx: &mut App) { - Root::update(self, cx, |root, window, cx| { - root.active_modals.clear(); - root.focus_back(window, cx); - cx.notify(); - }) - } - - fn push_notification(&mut self, note: impl Into, cx: &mut App) { - let note = note.into(); - Root::update(self, cx, move |root, window, cx| { - root.notification - .update(cx, |view, cx| view.push(note, window, cx)); - cx.notify(); - }) - } - - fn clear_notifications(&mut self, cx: &mut App) { - Root::update(self, cx, move |root, window, cx| { - root.notification - .update(cx, |view, cx| view.clear(window, cx)); - cx.notify(); - }) - } - - fn clear_notification_by_id(&mut self, id: SharedString, cx: &mut App) { - Root::update(self, cx, move |root, window, cx| { - root.notification.update(cx, |view, cx| { - view.close(id.clone(), window, cx); - }); - cx.notify(); - }) - } - - fn notifications(&mut self, cx: &mut App) -> Rc>> { - let entity = Root::read(self, cx).notification.clone(); - Rc::new(entity.read(cx).notifications()) - } - - fn has_focused_input(&mut self, cx: &mut App) -> bool { - Root::read(self, cx).focused_input.is_some() - } - - fn focused_input(&mut self, cx: &mut App) -> Option> { - Root::read(self, cx).focused_input.clone() - } -} - -type Builder = Rc Modal + 'static>; #[derive(Clone)] -pub(crate) struct ActiveModal { +#[allow(clippy::type_complexity)] +pub struct ActiveModal { focus_handle: FocusHandle, - builder: Builder, + /// The previous focused handle before opening the modal. + previous_focused_handle: Option, + builder: Rc Modal + 'static>, +} + +impl ActiveModal { + fn new( + focus_handle: FocusHandle, + previous_focused_handle: Option, + builder: impl Fn(Modal, &mut Window, &mut App) -> Modal + 'static, + ) -> Self { + Self { + focus_handle, + previous_focused_handle, + builder: Rc::new(builder), + } + } } /// Root is a view for the App window for as the top level view (Must be the first view in the window). /// /// It is used to manage the Modal, and Notification. pub struct Root { + /// All active models pub(crate) active_modals: Vec, - pub notification: Entity, - pub focused_input: Option>, - /// Used to store the focus handle of the previous view. - /// - /// When the Modal closes, we will focus back to the previous view. - previous_focus_handle: Option, + + /// Notification layer + pub(crate) notification: Entity, + + /// Current focused input + pub(crate) focused_input: Option>, + + /// App view view: AnyView, } impl Root { pub fn new(view: AnyView, window: &mut Window, cx: &mut Context) -> Self { Self { - previous_focus_handle: None, focused_input: None, active_modals: Vec::new(), notification: cx.new(|cx| NotificationList::new(window, cx)), @@ -188,13 +83,11 @@ impl Root { .read(cx) } - fn focus_back(&mut self, window: &mut Window, cx: &mut App) { - if let Some(handle) = self.previous_focus_handle.clone() { - window.focus(&handle, cx); - } + pub fn view(&self) -> &AnyView { + &self.view } - /// Render Notification layer. + /// Render the notification layer. pub fn render_notification_layer( window: &mut Window, cx: &mut App, @@ -210,10 +103,9 @@ impl Root { ) } - /// Render the Modal layer. + /// Render the modal layer. pub fn render_modal_layer(window: &mut Window, cx: &mut App) -> Option { let root = window.root::()??; - let active_modals = root.read(cx).active_modals.clone(); if active_modals.is_empty() { @@ -255,50 +147,271 @@ impl Root { Some(div().children(modals)) } - /// Return the root view of the Root. - pub fn view(&self) -> &AnyView { - &self.view + /// Open a modal. + pub fn open_modal(&mut self, builder: F, window: &mut Window, cx: &mut Context<'_, Self>) + where + F: Fn(Modal, &mut Window, &mut App) -> Modal + 'static, + { + let previous_focused_handle = window.focused(cx).map(|h| h.downgrade()); + let focus_handle = cx.focus_handle(); + focus_handle.focus(window, cx); + + self.active_modals.push(ActiveModal::new( + focus_handle, + previous_focused_handle, + builder, + )); + + cx.notify(); } - /// Replace the root view of the Root. - pub fn replace_view(&mut self, view: AnyView) { - self.view = view; + /// Close the topmost modal. + pub fn close_modal(&mut self, window: &mut Window, cx: &mut Context) { + self.focused_input = None; + + if let Some(handle) = self + .active_modals + .pop() + .and_then(|d| d.previous_focused_handle) + .and_then(|h| h.upgrade()) + { + window.focus(&handle, cx); + } + + cx.notify(); + } + + /// Close all modals. + pub fn close_all_modals(&mut self, window: &mut Window, cx: &mut Context) { + self.focused_input = None; + self.active_modals.clear(); + + let previous_focused_handle = self + .active_modals + .first() + .and_then(|d| d.previous_focused_handle.clone()); + + if let Some(handle) = previous_focused_handle.and_then(|h| h.upgrade()) { + window.focus(&handle, cx); + } + + cx.notify(); + } + + /// Check if there are any active modals. + pub fn has_active_modals(&self) -> bool { + !self.active_modals.is_empty() + } + + /// Push a notification to the notification layer. + pub fn push_notification(&mut self, note: T, window: &mut Window, cx: &mut Context<'_, Root>) + where + T: Into, + { + self.notification + .update(cx, |view, cx| view.push(note, window, cx)); + cx.notify(); + } + + pub fn clear_notification(&mut self, id: T, window: &mut Window, cx: &mut Context) + where + T: Into, + { + self.notification + .update(cx, |view, cx| view.close(id.into(), window, cx)); + cx.notify(); + } + + /// Clear all notifications from the notification layer. + pub fn clear_notifications(&mut self, window: &mut Window, cx: &mut Context<'_, Root>) { + self.notification + .update(cx, |view, cx| view.clear(window, cx)); + cx.notify(); } } impl Render for Root { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let base_font_size = cx.theme().font_size; + let rem_size = cx.theme().font_size; let font_family = cx.theme().font_family.clone(); let decorations = window.window_decorations(); - window.set_rem_size(base_font_size); + // Set the base font size + window.set_rem_size(rem_size); - window_border().child( - div() - .id("root") - .map(|this| match decorations { - Decorations::Server => this, - Decorations::Client { tiling, .. } => this - .when(!(tiling.top || tiling.right), |el| { - el.rounded_tr(CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!(tiling.top || tiling.left), |el| { - el.rounded_tl(CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!(tiling.bottom || tiling.right), |el| { - el.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!(tiling.bottom || tiling.left), |el| { - el.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING) - }), - }) - .relative() - .size_full() - .font_family(font_family) - .bg(cx.theme().background) - .text_color(cx.theme().text) - .child(self.view.clone()), - ) + // Set the client inset (linux only) + window.set_client_inset(CLIENT_SIDE_DECORATION_SHADOW); + + div() + .id("window") + .size_full() + .bg(gpui::transparent_black()) + .map(|div| match decorations { + Decorations::Server => div, + Decorations::Client { tiling } => div + .bg(gpui::transparent_black()) + .child( + canvas( + |_bounds, window, _cx| { + window.insert_hitbox( + Bounds::new( + point(px(0.0), px(0.0)), + window.window_bounds().get_bounds().size, + ), + HitboxBehavior::Normal, + ) + }, + move |_bounds, hitbox, window, _cx| { + let mouse = window.mouse_position(); + let size = window.window_bounds().get_bounds().size; + + let Some(edge) = + resize_edge(mouse, CLIENT_SIDE_DECORATION_SHADOW, size) + else { + return; + }; + + window.set_cursor_style( + match edge { + ResizeEdge::Top | ResizeEdge::Bottom => { + CursorStyle::ResizeUpDown + } + ResizeEdge::Left | ResizeEdge::Right => { + CursorStyle::ResizeLeftRight + } + ResizeEdge::TopLeft | ResizeEdge::BottomRight => { + CursorStyle::ResizeUpLeftDownRight + } + ResizeEdge::TopRight | ResizeEdge::BottomLeft => { + CursorStyle::ResizeUpRightDownLeft + } + }, + &hitbox, + ); + }, + ) + .size_full() + .absolute(), + ) + .when(!(tiling.top || tiling.right), |div| { + div.rounded_tr(CLIENT_SIDE_DECORATION_ROUNDING) + }) + .when(!(tiling.top || tiling.left), |div| { + div.rounded_tl(CLIENT_SIDE_DECORATION_ROUNDING) + }) + .when(!(tiling.bottom || tiling.right), |div| { + div.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING) + }) + .when(!(tiling.bottom || tiling.left), |div| { + div.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING) + }) + .when(!tiling.top, |div| div.pt(CLIENT_SIDE_DECORATION_SHADOW)) + .when(!tiling.bottom, |div| div.pb(CLIENT_SIDE_DECORATION_SHADOW)) + .when(!tiling.left, |div| div.pl(CLIENT_SIDE_DECORATION_SHADOW)) + .when(!tiling.right, |div| div.pr(CLIENT_SIDE_DECORATION_SHADOW)) + .on_mouse_down(MouseButton::Left, move |e, window, _cx| { + let size = window.window_bounds().get_bounds().size; + let pos = e.position; + + match resize_edge(pos, CLIENT_SIDE_DECORATION_SHADOW, size) { + Some(edge) => window.start_window_resize(edge), + None => window.start_window_move(), + }; + }), + }) + .child( + div() + .map(|div| match decorations { + Decorations::Server => div, + Decorations::Client { tiling } => div + .border_color(cx.theme().window_border) + .when(!(tiling.bottom || tiling.right), |div| { + div.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING) + }) + .when(!(tiling.bottom || tiling.left), |div| { + div.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING) + }) + .when(!tiling.top, |div| { + div.border_t(CLIENT_SIDE_DECORATION_BORDER) + }) + .when(!tiling.bottom, |div| { + div.border_b(CLIENT_SIDE_DECORATION_BORDER) + }) + .when(!tiling.left, |div| { + div.border_l(CLIENT_SIDE_DECORATION_BORDER) + }) + .when(!tiling.right, |div| { + div.border_r(CLIENT_SIDE_DECORATION_BORDER) + }) + .when(!tiling.is_tiled(), |div| { + div.shadow(vec![gpui::BoxShadow { + color: Hsla { + h: 0., + s: 0., + l: 0., + a: 0.4, + }, + blur_radius: CLIENT_SIDE_DECORATION_SHADOW / 2., + spread_radius: px(0.), + offset: point(px(0.0), px(0.0)), + }]) + }), + }) + .on_mouse_move(|_e, _, cx| { + cx.stop_propagation(); + }) + .size_full() + .font_family(font_family) + .bg(cx.theme().background) + .text_color(cx.theme().text) + .child(self.view.clone()), + ) } } + +/// Get the window paddings. +pub(crate) fn window_paddings(window: &Window, _cx: &App) -> Edges { + match window.window_decorations() { + Decorations::Server => Edges::all(px(0.0)), + Decorations::Client { tiling } => { + let mut paddings = Edges::all(CLIENT_SIDE_DECORATION_SHADOW); + if tiling.top { + paddings.top = px(0.0); + } + if tiling.bottom { + paddings.bottom = px(0.0); + } + if tiling.left { + paddings.left = px(0.0); + } + if tiling.right { + paddings.right = px(0.0); + } + paddings + } + } +} + +/// Get the window resize edge. +fn resize_edge(pos: Point, shadow_size: Pixels, size: Size) -> Option { + let edge = if pos.y < shadow_size && pos.x < shadow_size { + ResizeEdge::TopLeft + } else if pos.y < shadow_size && pos.x > size.width - shadow_size { + ResizeEdge::TopRight + } else if pos.y < shadow_size { + ResizeEdge::Top + } else if pos.y > size.height - shadow_size && pos.x < shadow_size { + ResizeEdge::BottomLeft + } else if pos.y > size.height - shadow_size && pos.x > size.width - shadow_size { + ResizeEdge::BottomRight + } else if pos.y > size.height - shadow_size { + ResizeEdge::Bottom + } else if pos.x < shadow_size { + ResizeEdge::Left + } else if pos.x > size.width - shadow_size { + ResizeEdge::Right + } else { + return None; + }; + Some(edge) +} diff --git a/crates/ui/src/window_border.rs b/crates/ui/src/window_border.rs deleted file mode 100644 index 8caddf3..0000000 --- a/crates/ui/src/window_border.rs +++ /dev/null @@ -1,204 +0,0 @@ -use gpui::prelude::FluentBuilder as _; -use gpui::{ - canvas, div, point, px, AnyElement, App, Bounds, CursorStyle, Decorations, Edges, - HitboxBehavior, Hsla, InteractiveElement as _, IntoElement, MouseButton, ParentElement, Pixels, - Point, RenderOnce, ResizeEdge, Size, Styled as _, Window, -}; -use theme::{CLIENT_SIDE_DECORATION_ROUNDING, CLIENT_SIDE_DECORATION_SHADOW}; - -const WINDOW_BORDER_WIDTH: Pixels = px(1.0); - -/// Create a new window border. -pub fn window_border() -> WindowBorder { - WindowBorder::new() -} - -/// Window border use to render a custom window border and shadow for Linux. -#[derive(IntoElement, Default)] -pub struct WindowBorder { - children: Vec, -} - -/// Get the window paddings. -pub fn window_paddings(window: &Window, _cx: &App) -> Edges { - match window.window_decorations() { - Decorations::Server => Edges::all(px(0.0)), - Decorations::Client { tiling } => { - let mut paddings = Edges::all(CLIENT_SIDE_DECORATION_SHADOW); - if tiling.top { - paddings.top = px(0.0); - } - if tiling.bottom { - paddings.bottom = px(0.0); - } - if tiling.left { - paddings.left = px(0.0); - } - if tiling.right { - paddings.right = px(0.0); - } - paddings - } - } -} - -impl WindowBorder { - pub fn new() -> Self { - Self { - ..Default::default() - } - } -} - -impl ParentElement for WindowBorder { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements); - } -} - -impl RenderOnce for WindowBorder { - fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement { - let decorations = window.window_decorations(); - window.set_client_inset(CLIENT_SIDE_DECORATION_SHADOW); - - div() - .id("window-backdrop") - .bg(gpui::transparent_black()) - .map(|div| match decorations { - Decorations::Server => div, - Decorations::Client { tiling, .. } => div - .bg(gpui::transparent_black()) - .child( - canvas( - |_bounds, window, _cx| { - window.insert_hitbox( - Bounds::new( - point(px(0.0), px(0.0)), - window.window_bounds().get_bounds().size, - ), - HitboxBehavior::Normal, - ) - }, - move |_bounds, hitbox, window, _cx| { - let mouse = window.mouse_position(); - let size = window.window_bounds().get_bounds().size; - let Some(edge) = - resize_edge(mouse, CLIENT_SIDE_DECORATION_SHADOW, size) - else { - return; - }; - window.set_cursor_style( - match edge { - ResizeEdge::Top | ResizeEdge::Bottom => { - CursorStyle::ResizeUpDown - } - ResizeEdge::Left | ResizeEdge::Right => { - CursorStyle::ResizeLeftRight - } - ResizeEdge::TopLeft | ResizeEdge::BottomRight => { - CursorStyle::ResizeUpLeftDownRight - } - ResizeEdge::TopRight | ResizeEdge::BottomLeft => { - CursorStyle::ResizeUpRightDownLeft - } - }, - &hitbox, - ); - }, - ) - .size_full() - .absolute(), - ) - .when(!(tiling.top || tiling.right), |div| { - div.rounded_tr(CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!(tiling.top || tiling.left), |div| { - div.rounded_tl(CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!(tiling.bottom || tiling.right), |div| { - div.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!(tiling.bottom || tiling.left), |div| { - div.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!tiling.top, |div| div.pt(CLIENT_SIDE_DECORATION_SHADOW)) - .when(!tiling.bottom, |div| div.pb(CLIENT_SIDE_DECORATION_SHADOW)) - .when(!tiling.left, |div| div.pl(CLIENT_SIDE_DECORATION_SHADOW)) - .when(!tiling.right, |div| div.pr(CLIENT_SIDE_DECORATION_SHADOW)) - .on_mouse_down(MouseButton::Left, move |_, window, _cx| { - let size = window.window_bounds().get_bounds().size; - let pos = window.mouse_position(); - - if let Some(edge) = resize_edge(pos, CLIENT_SIDE_DECORATION_SHADOW, size) { - window.start_window_resize(edge) - }; - }), - }) - .size_full() - .child( - div() - .map(|div| match decorations { - Decorations::Server => div, - Decorations::Client { tiling } => div - .when(!(tiling.top || tiling.right), |div| { - div.rounded_tr(CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!(tiling.top || tiling.left), |div| { - div.rounded_tl(CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!(tiling.bottom || tiling.right), |div| { - div.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!(tiling.bottom || tiling.left), |div| { - div.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!tiling.top, |div| div.border_t(WINDOW_BORDER_WIDTH)) - .when(!tiling.bottom, |div| div.border_b(WINDOW_BORDER_WIDTH)) - .when(!tiling.left, |div| div.border_l(WINDOW_BORDER_WIDTH)) - .when(!tiling.right, |div| div.border_r(WINDOW_BORDER_WIDTH)) - .when(!tiling.is_tiled(), |div| { - div.shadow(vec![gpui::BoxShadow { - color: Hsla { - h: 0., - s: 0., - l: 0., - a: 0.3, - }, - blur_radius: CLIENT_SIDE_DECORATION_SHADOW / 2., - spread_radius: px(0.), - offset: point(px(0.0), px(0.0)), - }]) - }), - }) - .on_mouse_move(|_e, _window, cx| { - cx.stop_propagation(); - }) - .bg(gpui::transparent_black()) - .size_full() - .children(self.children), - ) - } -} - -fn resize_edge(pos: Point, shadow_size: Pixels, size: Size) -> Option { - let edge = if pos.y < shadow_size && pos.x < shadow_size { - ResizeEdge::TopLeft - } else if pos.y < shadow_size && pos.x > size.width - shadow_size { - ResizeEdge::TopRight - } else if pos.y < shadow_size { - ResizeEdge::Top - } else if pos.y > size.height - shadow_size && pos.x < shadow_size { - ResizeEdge::BottomLeft - } else if pos.y > size.height - shadow_size && pos.x > size.width - shadow_size { - ResizeEdge::BottomRight - } else if pos.y > size.height - shadow_size { - ResizeEdge::Bottom - } else if pos.x < shadow_size { - ResizeEdge::Left - } else if pos.x > size.width - shadow_size { - ResizeEdge::Right - } else { - return None; - }; - Some(edge) -} diff --git a/crates/ui/src/window_ext.rs b/crates/ui/src/window_ext.rs new file mode 100644 index 0000000..dd71dd7 --- /dev/null +++ b/crates/ui/src/window_ext.rs @@ -0,0 +1,120 @@ +use std::rc::Rc; + +use gpui::{App, Entity, SharedString, Window}; + +use crate::input::InputState; +use crate::modal::Modal; +use crate::notification::Notification; +use crate::Root; + +/// Extension trait for [`Window`] to add modal, notification .. functionality. +pub trait WindowExtension: Sized { + /// Opens a Modal. + fn open_modal(&mut self, cx: &mut App, builder: F) + where + F: Fn(Modal, &mut Window, &mut App) -> Modal + 'static; + + /// Return true, if there is an active Modal. + fn has_active_modal(&mut self, cx: &mut App) -> bool; + + /// Closes the last active Modal. + fn close_modal(&mut self, cx: &mut App); + + /// Closes all active Modals. + fn close_all_modals(&mut self, cx: &mut App); + + /// Returns number of notifications. + fn notifications(&mut self, cx: &mut App) -> Rc>>; + + /// Pushes a notification to the notification list. + fn push_notification(&mut self, note: T, cx: &mut App) + where + T: Into; + + /// Clears a notification by its ID. + fn clear_notification(&mut self, id: T, cx: &mut App) + where + T: Into; + + /// Clear all notifications + fn clear_notifications(&mut self, cx: &mut App); + + /// Return current focused Input entity. + fn focused_input(&mut self, cx: &mut App) -> Option>; + + /// Returns true if there is a focused Input entity. + fn has_focused_input(&mut self, cx: &mut App) -> bool; +} + +impl WindowExtension for Window { + #[inline] + fn open_modal(&mut self, cx: &mut App, builder: F) + where + F: Fn(Modal, &mut Window, &mut App) -> Modal + 'static, + { + Root::update(self, cx, move |root, window, cx| { + root.open_modal(builder, window, cx); + }) + } + + #[inline] + fn has_active_modal(&mut self, cx: &mut App) -> bool { + Root::read(self, cx).has_active_modals() + } + + #[inline] + fn close_modal(&mut self, cx: &mut App) { + Root::update(self, cx, move |root, window, cx| { + root.close_modal(window, cx); + }) + } + + #[inline] + fn close_all_modals(&mut self, cx: &mut App) { + Root::update(self, cx, |root, window, cx| { + root.close_all_modals(window, cx); + }) + } + + #[inline] + fn push_notification(&mut self, note: T, cx: &mut App) + where + T: Into, + { + let note = note.into(); + Root::update(self, cx, move |root, window, cx| { + root.push_notification(note, window, cx); + }) + } + + #[inline] + fn clear_notification(&mut self, id: T, cx: &mut App) + where + T: Into, + { + let id = id.into(); + Root::update(self, cx, move |root, window, cx| { + root.clear_notification(id, window, cx); + }) + } + + #[inline] + fn clear_notifications(&mut self, cx: &mut App) { + Root::update(self, cx, move |root, window, cx| { + root.clear_notifications(window, cx); + }) + } + + fn notifications(&mut self, cx: &mut App) -> Rc>> { + let entity = Root::read(self, cx).notification.clone(); + Rc::new(entity.read(cx).notifications()) + } + + fn has_focused_input(&mut self, cx: &mut App) -> bool { + Root::read(self, cx).focused_input.is_some() + } + + fn focused_input(&mut self, cx: &mut App) -> Option> { + Root::read(self, cx).focused_input.clone() + } +} -- 2.49.1 From ca38cc23d92b8840d83dc48388548f955e2f11a1 Mon Sep 17 00:00:00 2001 From: reya Date: Fri, 16 Jan 2026 18:39:57 +0700 Subject: [PATCH 04/30] improve title bar --- crates/coop/src/actions.rs | 31 ------------- crates/coop/src/main.rs | 39 +++++++++++++--- crates/title_bar/src/lib.rs | 2 +- crates/title_bar/src/platforms/linux.rs | 59 +++++++++++-------------- crates/ui/src/root.rs | 6 +++ 5 files changed, 66 insertions(+), 71 deletions(-) diff --git a/crates/coop/src/actions.rs b/crates/coop/src/actions.rs index 8a5798b..d69eced 100644 --- a/crates/coop/src/actions.rs +++ b/crates/coop/src/actions.rs @@ -1,5 +1,3 @@ -use std::sync::Mutex; - use gpui::{actions, App}; use key_store::{KeyItem, KeyStore}; use nostr_connect::prelude::*; @@ -37,30 +35,6 @@ impl AuthUrlHandler for CoopAuthUrlHandler { } } -pub fn load_embedded_fonts(cx: &App) { - let asset_source = cx.asset_source(); - let font_paths = asset_source.list("fonts").unwrap(); - let embedded_fonts = Mutex::new(Vec::new()); - let executor = cx.background_executor(); - - cx.foreground_executor().block_on(executor.scoped(|scope| { - for font_path in &font_paths { - if !font_path.ends_with(".ttf") { - continue; - } - - scope.spawn(async { - let font_bytes = asset_source.load(font_path).unwrap().unwrap(); - embedded_fonts.lock().unwrap().push(font_bytes); - }); - } - })); - - cx.text_system() - .add_fonts(embedded_fonts.into_inner().unwrap()) - .unwrap(); -} - pub fn reset(cx: &mut App) { let backend = KeyStore::global(cx).read(cx).backend(); let client = NostrRegistry::global(cx).read(cx).client(); @@ -87,8 +61,3 @@ pub fn reset(cx: &mut App) { }) .detach(); } - -pub fn quit(_: &Quit, cx: &mut App) { - log::info!("Gracefully quitting the application . . ."); - cx.quit(); -} diff --git a/crates/coop/src/main.rs b/crates/coop/src/main.rs index 3777c3a..6fe4ec0 100644 --- a/crates/coop/src/main.rs +++ b/crates/coop/src/main.rs @@ -1,15 +1,15 @@ -use std::sync::Arc; +use std::sync::{Arc, Mutex}; use assets::Assets; use common::{APP_ID, CLIENT_NAME}; use gpui::{ - point, px, size, AppContext, Application, Bounds, KeyBinding, Menu, MenuItem, SharedString, - Size, TitlebarOptions, WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, - WindowOptions, + point, px, size, App, AppContext, Application, Bounds, KeyBinding, Menu, MenuItem, + SharedString, Size, TitlebarOptions, WindowBackgroundAppearance, WindowBounds, + WindowDecorations, WindowKind, WindowOptions, }; use ui::Root; -use crate::actions::{load_embedded_fonts, quit, Quit}; +use crate::actions::Quit; mod actions; mod chatspace; @@ -117,3 +117,32 @@ fn main() { .expect("Failed to open window. Please restart the application."); }); } + +fn load_embedded_fonts(cx: &App) { + let asset_source = cx.asset_source(); + let font_paths = asset_source.list("fonts").unwrap(); + let embedded_fonts = Mutex::new(vec![]); + let executor = cx.background_executor(); + + cx.foreground_executor().block_on(executor.scoped(|scope| { + for font_path in &font_paths { + if !font_path.ends_with(".ttf") { + continue; + } + + scope.spawn(async { + let font_bytes = asset_source.load(font_path).unwrap().unwrap(); + embedded_fonts.lock().unwrap().push(font_bytes); + }); + } + })); + + cx.text_system() + .add_fonts(embedded_fonts.into_inner().unwrap()) + .unwrap(); +} + +fn quit(_ev: &Quit, cx: &mut App) { + log::info!("Gracefully quitting the application . . ."); + cx.quit(); +} diff --git a/crates/title_bar/src/lib.rs b/crates/title_bar/src/lib.rs index f4f9471..0ff981a 100644 --- a/crates/title_bar/src/lib.rs +++ b/crates/title_bar/src/lib.rs @@ -143,7 +143,7 @@ impl Render for TitleBar { PlatformKind::Linux => { #[cfg(target_os = "linux")] if matches!(decorations, Decorations::Client { .. }) { - this.child(LinuxWindowControls::new(None)) + this.child(LinuxWindowControls::new()) .when(supported_controls.window_menu, |this| { this.on_mouse_down(MouseButton::Right, move |ev, window, _| { window.show_window_menu(ev.position) diff --git a/crates/title_bar/src/platforms/linux.rs b/crates/title_bar/src/platforms/linux.rs index b164b6f..ba12119 100644 --- a/crates/title_bar/src/platforms/linux.rs +++ b/crates/title_bar/src/platforms/linux.rs @@ -4,23 +4,19 @@ use std::sync::OnceLock; use gpui::prelude::FluentBuilder; use gpui::{ - img, Action, App, InteractiveElement, IntoElement, MouseButton, ParentElement, RenderOnce, - StatefulInteractiveElement, Styled, Window, + svg, App, InteractiveElement, IntoElement, MouseButton, ParentElement, RenderOnce, + SharedString, StatefulInteractiveElement, Styled, Window, }; use linicon::{lookup_icon, IconType}; use theme::ActiveTheme; use ui::{h_flex, Icon, IconName, Sizable}; #[derive(IntoElement)] -pub struct LinuxWindowControls { - close_window_action: Option>, -} +pub struct LinuxWindowControls {} impl LinuxWindowControls { - pub fn new(close_window_action: Option>) -> Self { - Self { - close_window_action, - } + pub fn new() -> Self { + Self {} } } @@ -42,12 +38,10 @@ impl RenderOnce for LinuxWindowControls { WindowControl::new(LinuxControl::Maximize, IconName::WindowMaximize) } }) - .child( - WindowControl::new(LinuxControl::Close, IconName::WindowClose) - .when_some(self.close_window_action, |this, close_action| { - this.close_action(close_action) - }), - ) + .child(WindowControl::new( + LinuxControl::Close, + IconName::WindowClose, + )) } } @@ -55,21 +49,11 @@ impl RenderOnce for LinuxWindowControls { pub struct WindowControl { kind: LinuxControl, fallback: IconName, - close_action: Option>, } impl WindowControl { pub fn new(kind: LinuxControl, fallback: IconName) -> Self { - Self { - kind, - fallback, - close_action: None, - } - } - - pub fn close_action(mut self, action: Box) -> Self { - self.close_action = Some(action); - self + Self { kind, fallback } } pub fn is_gnome(&self) -> bool { @@ -102,7 +86,20 @@ impl RenderOnce for WindowControl { }) .map(|this| { if let Some(Some(path)) = linux_controls().get(&self.kind).cloned() { - this.child(img(path).flex_grow().size_4()) + this.child( + svg() + .external_path(SharedString::from( + path.into_os_string().into_string().unwrap(), + )) + .map(|this| { + if cx.theme().is_dark() { + this.text_color(gpui::white()) + } else { + this.text_color(gpui::black()) + } + }) + .size_4(), + ) } else { this.child(Icon::new(self.fallback).flex_grow().small()) } @@ -114,13 +111,7 @@ impl RenderOnce for WindowControl { LinuxControl::Minimize => window.minimize_window(), LinuxControl::Restore => window.zoom_window(), LinuxControl::Maximize => window.zoom_window(), - LinuxControl::Close => window.dispatch_action( - self.close_action - .as_ref() - .expect("Use WindowControl::new_close() for close control.") - .boxed_clone(), - cx, - ), + LinuxControl::Close => {} } }) } diff --git a/crates/ui/src/root.rs b/crates/ui/src/root.rs index 0998591..a69775d 100644 --- a/crates/ui/src/root.rs +++ b/crates/ui/src/root.rs @@ -325,6 +325,12 @@ impl Render for Root { Decorations::Server => div, Decorations::Client { tiling } => div .border_color(cx.theme().window_border) + .when(!(tiling.top || tiling.right), |div| { + div.rounded_tr(CLIENT_SIDE_DECORATION_ROUNDING) + }) + .when(!(tiling.top || tiling.left), |div| { + div.rounded_tl(CLIENT_SIDE_DECORATION_ROUNDING) + }) .when(!(tiling.bottom || tiling.right), |div| { div.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING) }) -- 2.49.1 From f5fcabd1aa0640818fada275c9dbb1744a9b56a8 Mon Sep 17 00:00:00 2001 From: reya Date: Sat, 17 Jan 2026 09:08:24 +0700 Subject: [PATCH 05/30] fix titlebar --- crates/title_bar/src/platforms/linux.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/title_bar/src/platforms/linux.rs b/crates/title_bar/src/platforms/linux.rs index ba12119..d285d71 100644 --- a/crates/title_bar/src/platforms/linux.rs +++ b/crates/title_bar/src/platforms/linux.rs @@ -24,7 +24,6 @@ impl RenderOnce for LinuxWindowControls { fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement { h_flex() .id("linux-window-controls") - .px_2() .gap_2() .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) .child(WindowControl::new( @@ -104,14 +103,14 @@ impl RenderOnce for WindowControl { this.child(Icon::new(self.fallback).flex_grow().small()) } }) - .on_mouse_move(|_, _window, cx| cx.stop_propagation()) - .on_click(move |_, window, cx| { + .on_mouse_move(|_ev, _window, cx| cx.stop_propagation()) + .on_click(move |_ev, window, cx| { cx.stop_propagation(); match self.kind { LinuxControl::Minimize => window.minimize_window(), LinuxControl::Restore => window.zoom_window(), LinuxControl::Maximize => window.zoom_window(), - LinuxControl::Close => {} + LinuxControl::Close => cx.quit(), } }) } -- 2.49.1 From 178a76355236c5e7e6de381060382c7def01d038 Mon Sep 17 00:00:00 2001 From: reya Date: Mon, 19 Jan 2026 09:25:15 +0700 Subject: [PATCH 06/30] fix tiling window --- Cargo.lock | 161 ++++++++++++++++++++++-------------------- crates/ui/src/root.rs | 5 +- 2 files changed, 88 insertions(+), 78 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a4d1561..1b0e8c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -477,7 +477,7 @@ dependencies = [ "crc32fast", "futures-lite 2.6.1", "pin-project", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -537,7 +537,7 @@ dependencies = [ "num-traits", "pastey", "rayon", - "thiserror 2.0.17", + "thiserror 2.0.18", "v_frame", "y4m", ] @@ -934,9 +934,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.52" +version = "1.2.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" +checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932" dependencies = [ "find-msvc-tools", "jobserver", @@ -1179,7 +1179,7 @@ dependencies = [ [[package]] name = "collections" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#83ca31055cf3e56aa8a704ac49e1686434f4e640" +source = "git+https://github.com/zed-industries/zed#e476af6417576903700f8a7645a7de3315b5ff6c" dependencies = [ "indexmap", "rustc-hash 2.1.1", @@ -1208,7 +1208,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f849b92c694fe237ecd8fafd1ba0df7ae0d45c1df6daeb7f68ed4220d51640bd" dependencies = [ "nix 0.30.1", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -1373,6 +1373,19 @@ dependencies = [ "libc", ] +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.10.0", + "core-graphics-types 0.2.0", + "foreign-types 0.5.0", + "libc", +] + [[package]] name = "core-graphics-helmer-fork" version = "0.24.0" @@ -1423,14 +1436,13 @@ dependencies = [ [[package]] name = "core-text" -version = "21.0.0" +version = "21.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a593227b66cbd4007b2a050dfdd9e1d1318311409c8d600dc82ba1b15ca9c130" +checksum = "fce32d657e17d6e4a8e70fe2ae6875218015f320620a78e5949d228bc76622bd" dependencies = [ "core-foundation 0.10.0", - "core-graphics 0.24.0", + "core-graphics 0.25.0", "foreign-types 0.5.0", - "libc", ] [[package]] @@ -1607,7 +1619,7 @@ dependencies = [ [[package]] name = "derive_refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#83ca31055cf3e56aa8a704ac49e1686434f4e640" +source = "git+https://github.com/zed-industries/zed#e476af6417576903700f8a7645a7de3315b5ff6c" dependencies = [ "proc-macro2", "quote", @@ -2022,21 +2034,20 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.26" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" dependencies = [ "cfg-if", "libc", "libredox", - "windows-sys 0.60.2", ] [[package]] name = "find-msvc-tools" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" +checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" [[package]] name = "flatbuffers" @@ -2517,7 +2528,7 @@ dependencies = [ [[package]] name = "gpui" version = "0.2.2" -source = "git+https://github.com/zed-industries/zed#83ca31055cf3e56aa8a704ac49e1686434f4e640" +source = "git+https://github.com/zed-industries/zed#e476af6417576903700f8a7645a7de3315b5ff6c" dependencies = [ "anyhow", "as-raw-xcb-connection", @@ -2592,7 +2603,7 @@ dependencies = [ "sum_tree", "swash", "taffy", - "thiserror 2.0.17", + "thiserror 2.0.18", "usvg", "util", "util_macros", @@ -2619,7 +2630,7 @@ dependencies = [ [[package]] name = "gpui_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#83ca31055cf3e56aa8a704ac49e1686434f4e640" +source = "git+https://github.com/zed-industries/zed#e476af6417576903700f8a7645a7de3315b5ff6c" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -2630,7 +2641,7 @@ dependencies = [ [[package]] name = "gpui_tokio" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#83ca31055cf3e56aa8a704ac49e1686434f4e640" +source = "git+https://github.com/zed-industries/zed#e476af6417576903700f8a7645a7de3315b5ff6c" dependencies = [ "anyhow", "gpui", @@ -2852,7 +2863,7 @@ dependencies = [ [[package]] name = "http_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#83ca31055cf3e56aa8a704ac49e1686434f4e640" +source = "git+https://github.com/zed-industries/zed#e476af6417576903700f8a7645a7de3315b5ff6c" dependencies = [ "anyhow", "async-compression", @@ -2877,7 +2888,7 @@ dependencies = [ [[package]] name = "http_client_tls" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#83ca31055cf3e56aa8a704ac49e1686434f4e640" +source = "git+https://github.com/zed-industries/zed#e476af6417576903700f8a7645a7de3315b5ff6c" dependencies = [ "rustls", "rustls-platform-verifier", @@ -3116,8 +3127,8 @@ dependencies = [ "rayon", "rgb", "tiff", - "zune-core 0.5.0", - "zune-jpeg 0.5.8", + "zune-core 0.5.1", + "zune-jpeg 0.5.10", ] [[package]] @@ -3638,7 +3649,7 @@ dependencies = [ [[package]] name = "media" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#83ca31055cf3e56aa8a704ac49e1686434f4e640" +source = "git+https://github.com/zed-industries/zed#e476af6417576903700f8a7645a7de3315b5ff6c" dependencies = [ "anyhow", "bindgen", @@ -3778,7 +3789,7 @@ dependencies = [ "rustc-hash 1.1.0", "spirv", "strum 0.26.3", - "thiserror 2.0.17", + "thiserror 2.0.18", "unicode-ident", ] @@ -3878,7 +3889,7 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" [[package]] name = "nostr" version = "0.44.1" -source = "git+https://github.com/rust-nostr/nostr#78e3b86e736c470ca7241e7383d3886721fc64c2" +source = "git+https://github.com/rust-nostr/nostr#6d776ad79052ec908ffdabfe4229281d917abb59" dependencies = [ "aes", "base64", @@ -3903,7 +3914,7 @@ dependencies = [ [[package]] name = "nostr-connect" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#78e3b86e736c470ca7241e7383d3886721fc64c2" +source = "git+https://github.com/rust-nostr/nostr#6d776ad79052ec908ffdabfe4229281d917abb59" dependencies = [ "async-utility", "nostr", @@ -3915,7 +3926,7 @@ dependencies = [ [[package]] name = "nostr-database" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#78e3b86e736c470ca7241e7383d3886721fc64c2" +source = "git+https://github.com/rust-nostr/nostr#6d776ad79052ec908ffdabfe4229281d917abb59" dependencies = [ "btreecap", "flatbuffers", @@ -3927,7 +3938,7 @@ dependencies = [ [[package]] name = "nostr-gossip" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#78e3b86e736c470ca7241e7383d3886721fc64c2" +source = "git+https://github.com/rust-nostr/nostr#6d776ad79052ec908ffdabfe4229281d917abb59" dependencies = [ "nostr", ] @@ -3935,7 +3946,7 @@ dependencies = [ [[package]] name = "nostr-lmdb" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#78e3b86e736c470ca7241e7383d3886721fc64c2" +source = "git+https://github.com/rust-nostr/nostr#6d776ad79052ec908ffdabfe4229281d917abb59" dependencies = [ "async-utility", "flume", @@ -3949,7 +3960,7 @@ dependencies = [ [[package]] name = "nostr-relay-pool" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#78e3b86e736c470ca7241e7383d3886721fc64c2" +source = "git+https://github.com/rust-nostr/nostr#6d776ad79052ec908ffdabfe4229281d917abb59" dependencies = [ "async-utility", "async-wsocket", @@ -3966,7 +3977,7 @@ dependencies = [ [[package]] name = "nostr-sdk" version = "0.44.1" -source = "git+https://github.com/rust-nostr/nostr#78e3b86e736c470ca7241e7383d3886721fc64c2" +source = "git+https://github.com/rust-nostr/nostr#6d776ad79052ec908ffdabfe4229281d917abb59" dependencies = [ "async-utility", "nostr", @@ -4498,7 +4509,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perf" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#83ca31055cf3e56aa8a704ac49e1686434f4e640" +source = "git+https://github.com/zed-industries/zed#e476af6417576903700f8a7645a7de3315b5ff6c" dependencies = [ "collections", "serde", @@ -4853,7 +4864,7 @@ dependencies = [ "rustc-hash 2.1.1", "rustls", "socket2", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", "web-time", @@ -4874,7 +4885,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.17", + "thiserror 2.0.18", "tinyvec", "tracing", "web-time", @@ -5004,7 +5015,7 @@ dependencies = [ "rand 0.9.2", "rand_chacha 0.9.0", "simd_helpers", - "thiserror 2.0.17", + "thiserror 2.0.18", "v_frame", "wasm-bindgen", ] @@ -5133,7 +5144,7 @@ dependencies = [ [[package]] name = "refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#83ca31055cf3e56aa8a704ac49e1686434f4e640" +source = "git+https://github.com/zed-industries/zed#e476af6417576903700f8a7645a7de3315b5ff6c" dependencies = [ "derive_refineable", ] @@ -5232,7 +5243,7 @@ dependencies = [ [[package]] name = "reqwest_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#83ca31055cf3e56aa8a704ac49e1686434f4e640" +source = "git+https://github.com/zed-industries/zed#e476af6417576903700f8a7645a7de3315b5ff6c" dependencies = [ "anyhow", "bytes", @@ -5286,7 +5297,7 @@ dependencies = [ [[package]] name = "rope" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#83ca31055cf3e56aa8a704ac49e1686434f4e640" +source = "git+https://github.com/zed-industries/zed#e476af6417576903700f8a7645a7de3315b5ff6c" dependencies = [ "arrayvec", "log", @@ -5351,9 +5362,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" [[package]] name = "rustc-hash" @@ -5441,9 +5452,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.2" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ "web-time", "zeroize", @@ -5478,9 +5489,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.8" +version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ "aws-lc-rs", "ring", @@ -5565,7 +5576,7 @@ dependencies = [ [[package]] name = "scheduler" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#83ca31055cf3e56aa8a704ac49e1686434f4e640" +source = "git+https://github.com/zed-industries/zed#e476af6417576903700f8a7645a7de3315b5ff6c" dependencies = [ "async-task", "backtrace", @@ -6155,7 +6166,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sum_tree" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#83ca31055cf3e56aa8a704ac49e1686434f4e640" +source = "git+https://github.com/zed-industries/zed#e476af6417576903700f8a7645a7de3315b5ff6c" dependencies = [ "arrayvec", "log", @@ -6452,11 +6463,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.18", ] [[package]] @@ -6472,9 +6483,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -6905,7 +6916,7 @@ dependencies = [ "rustls", "rustls-pki-types", "sha1", - "thiserror 2.0.17", + "thiserror 2.0.18", "utf-8", ] @@ -7113,7 +7124,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "util" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#83ca31055cf3e56aa8a704ac49e1686434f4e640" +source = "git+https://github.com/zed-industries/zed#e476af6417576903700f8a7645a7de3315b5ff6c" dependencies = [ "anyhow", "async-fs", @@ -7149,7 +7160,7 @@ dependencies = [ [[package]] name = "util_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#83ca31055cf3e56aa8a704ac49e1686434f4e640" +source = "git+https://github.com/zed-industries/zed#e476af6417576903700f8a7645a7de3315b5ff6c" dependencies = [ "perf", "quote", @@ -7287,9 +7298,9 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ "wit-bindgen", ] @@ -7633,7 +7644,7 @@ checksum = "3a4df73e95feddb9ec1a7e9c2ca6323b8c97d5eeeff78d28f1eccdf19c882b24" dependencies = [ "parking_lot", "rayon", - "thiserror 2.0.17", + "thiserror 2.0.18", "windows 0.61.3", "windows-future", ] @@ -8191,9 +8202,9 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" [[package]] name = "writeable" @@ -8625,7 +8636,7 @@ dependencies = [ [[package]] name = "zlog" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#83ca31055cf3e56aa8a704ac49e1686434f4e640" +source = "git+https://github.com/zed-industries/zed#e476af6417576903700f8a7645a7de3315b5ff6c" dependencies = [ "anyhow", "chrono", @@ -8635,14 +8646,14 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea" +checksum = "94f63c051f4fe3c1509da62131a678643c5b6fbdc9273b2b79d4378ebda003d2" [[package]] name = "ztracing" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#83ca31055cf3e56aa8a704ac49e1686434f4e640" +source = "git+https://github.com/zed-industries/zed#e476af6417576903700f8a7645a7de3315b5ff6c" dependencies = [ "tracing", "tracing-subscriber", @@ -8653,7 +8664,7 @@ dependencies = [ [[package]] name = "ztracing_macro" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#83ca31055cf3e56aa8a704ac49e1686434f4e640" +source = "git+https://github.com/zed-industries/zed#e476af6417576903700f8a7645a7de3315b5ff6c" [[package]] name = "zune-core" @@ -8663,9 +8674,9 @@ checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" [[package]] name = "zune-core" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "111f7d9820f05fd715df3144e254d6fc02ee4088b0644c0ffd0efc9e6d9d2773" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" [[package]] name = "zune-inflate" @@ -8687,18 +8698,18 @@ dependencies = [ [[package]] name = "zune-jpeg" -version = "0.5.8" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e35aee689668bf9bd6f6f3a6c60bb29ba1244b3b43adfd50edd554a371da37d5" +checksum = "ea2db9186c0a6ad1aa7012046f3fadc8db9001691b367c510f5867f17f975752" dependencies = [ - "zune-core 0.5.0", + "zune-core 0.5.1", ] [[package]] name = "zvariant" -version = "5.9.1" +version = "5.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "326aaed414f04fe839777b4c443d4e94c74e7b3621093bd9c5e649ac8aa96543" +checksum = "68b64ef4f40c7951337ddc7023dd03528a57a3ce3408ee9da5e948bd29b232c4" dependencies = [ "endi", "enumflags2", @@ -8711,9 +8722,9 @@ dependencies = [ [[package]] name = "zvariant_derive" -version = "5.9.1" +version = "5.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba44e1f8f4da9e6e2d25d2a60b116ef8b9d0be174a7685e55bb12a99866279a7" +checksum = "484d5d975eb7afb52cc6b929c13d3719a20ad650fea4120e6310de3fc55e415c" dependencies = [ "proc-macro-crate", "proc-macro2", diff --git a/crates/ui/src/root.rs b/crates/ui/src/root.rs index a69775d..2173387 100644 --- a/crates/ui/src/root.rs +++ b/crates/ui/src/root.rs @@ -313,9 +313,8 @@ impl Render for Root { let size = window.window_bounds().get_bounds().size; let pos = e.position; - match resize_edge(pos, CLIENT_SIDE_DECORATION_SHADOW, size) { - Some(edge) => window.start_window_resize(edge), - None => window.start_window_move(), + if let Some(edge) = resize_edge(pos, CLIENT_SIDE_DECORATION_SHADOW, size) { + window.start_window_resize(edge) }; }), }) -- 2.49.1 From ed403188f840376dc2d1c2a7b92a03ff12fe9ff5 Mon Sep 17 00:00:00 2001 From: reya Date: Mon, 19 Jan 2026 17:17:28 +0700 Subject: [PATCH 07/30] wip --- Cargo.lock | 3 +- crates/coop/Cargo.toml | 1 - crates/coop/src/actions.rs | 15 -- crates/coop/src/chatspace.rs | 178 ++------------ crates/coop/src/login/mod.rs | 4 +- crates/coop/src/views/mod.rs | 1 - crates/coop/src/views/onboarding.rs | 363 ---------------------------- crates/coop/src/views/startup.rs | 4 +- crates/state/Cargo.toml | 2 + crates/state/src/lib.rs | 59 ++++- 10 files changed, 88 insertions(+), 542 deletions(-) delete mode 100644 crates/coop/src/views/onboarding.rs diff --git a/Cargo.lock b/Cargo.lock index 1b0e8c4..6da7c4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1318,7 +1318,6 @@ dependencies = [ "title_bar", "tracing-subscriber", "ui", - "webbrowser", ] [[package]] @@ -6093,10 +6092,12 @@ dependencies = [ "flume", "gpui", "log", + "nostr-connect", "nostr-lmdb", "nostr-sdk", "rustls", "smol", + "webbrowser", ] [[package]] diff --git a/crates/coop/Cargo.toml b/crates/coop/Cargo.toml index a437497..a5e5ca4 100644 --- a/crates/coop/Cargo.toml +++ b/crates/coop/Cargo.toml @@ -58,7 +58,6 @@ smallvec.workspace = true smol.workspace = true futures.workspace = true oneshot.workspace = true -webbrowser.workspace = true indexset = "0.12.3" tracing-subscriber = { version = "0.3.18", features = ["fmt"] } diff --git a/crates/coop/src/actions.rs b/crates/coop/src/actions.rs index d69eced..a6cbdf8 100644 --- a/crates/coop/src/actions.rs +++ b/crates/coop/src/actions.rs @@ -1,6 +1,5 @@ use gpui::{actions, App}; use key_store::{KeyItem, KeyStore}; -use nostr_connect::prelude::*; use state::NostrRegistry; // Sidebar actions @@ -21,20 +20,6 @@ actions!( ] ); -#[derive(Debug, Clone)] -pub struct CoopAuthUrlHandler; - -impl AuthUrlHandler for CoopAuthUrlHandler { - #[allow(mismatched_lifetime_syntaxes)] - fn on_auth_url(&self, auth_url: Url) -> BoxedFuture> { - Box::pin(async move { - log::info!("Received Auth URL: {auth_url}"); - webbrowser::open(auth_url.as_str())?; - Ok(()) - }) - } -} - pub fn reset(cx: &mut App) { let backend = KeyStore::global(cx).read(cx).backend(); let client = NostrRegistry::global(cx).read(cx).client(); diff --git a/crates/coop/src/chatspace.rs b/crates/coop/src/chatspace.rs index 6f85925..6ae1e2b 100644 --- a/crates/coop/src/chatspace.rs +++ b/crates/coop/src/chatspace.rs @@ -10,7 +10,6 @@ use gpui::{ InteractiveElement, IntoElement, ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Window, }; -use key_store::{Credential, KeyItem, KeyStore}; use nostr_connect::prelude::*; use person::PersonRegistry; use relay_auth::RelayAuth; @@ -21,34 +20,23 @@ use title_bar::TitleBar; use ui::avatar::Avatar; use ui::button::{Button, ButtonVariants}; use ui::dock_area::dock::DockPlacement; -use ui::dock_area::panel::PanelView; use ui::dock_area::{ClosePanel, DockArea, DockItem}; use ui::modal::ModalButtonProps; use ui::popup_menu::PopupMenuExt; -use ui::{h_flex, v_flex, IconName, Root, Sizable, StyledExt, WindowExtension}; +use ui::{h_flex, v_flex, IconName, Root, Sizable, WindowExtension}; use crate::actions::{ reset, DarkMode, KeyringPopup, Logout, Settings, Themes, ViewProfile, ViewRelays, }; use crate::user::viewer; use crate::views::compose::compose_button; -use crate::views::{onboarding, preferences, setup_relay, startup, welcome}; -use crate::{login, new_identity, sidebar, user}; +use crate::views::{preferences, setup_relay, welcome}; +use crate::{login, sidebar, user}; pub fn init(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| ChatSpace::new(window, cx)) } -pub fn login(window: &mut Window, cx: &mut App) { - let panel = login::init(window, cx); - ChatSpace::set_center_panel(panel, window, cx); -} - -pub fn new_account(window: &mut Window, cx: &mut App) { - let panel = new_identity::init(window, cx); - ChatSpace::set_center_panel(panel, window, cx); -} - #[derive(Debug)] pub struct ChatSpace { /// App's Title Bar @@ -61,20 +49,15 @@ pub struct ChatSpace { ready: bool, /// Event subscriptions - _subscriptions: SmallVec<[Subscription; 4]>, + _subscriptions: SmallVec<[Subscription; 3]>, } impl ChatSpace { - pub fn new(window: &mut Window, cx: &mut Context) -> Self { - let nostr = NostrRegistry::global(cx); + fn new(window: &mut Window, cx: &mut Context) -> Self { let chat = ChatRegistry::global(cx); - let keystore = KeyStore::global(cx); - let title_bar = cx.new(|_| TitleBar::new()); let dock = cx.new(|cx| DockArea::new(window, cx)); - let identity = nostr.read(cx).identity(); - let mut subscriptions = smallvec![]; subscriptions.push( @@ -84,50 +67,6 @@ impl ChatSpace { }), ); - subscriptions.push( - // Observe account entity changes - cx.observe_in(&identity, window, move |this, state, window, cx| { - if !this.ready && state.read(cx).has_public_key() { - this.set_default_layout(window, cx); - - // Load all chat room in the database if available - let chat = ChatRegistry::global(cx); - chat.update(cx, |this, cx| { - this.get_rooms(cx); - }); - }; - }), - ); - - subscriptions.push( - // Observe keystore entity changes - cx.observe_in(&keystore, window, move |_this, state, window, cx| { - if state.read(cx).initialized { - let backend = state.read(cx).backend(); - - cx.spawn_in(window, async move |this, cx| { - let result = backend - .read_credentials(&KeyItem::User.to_string(), cx) - .await; - - this.update_in(cx, |this, window, cx| { - match result { - Ok(Some((user, secret))) => { - let credential = Credential::new(user, secret); - this.set_startup_layout(credential, window, cx); - } - _ => { - this.set_onboarding_layout(window, cx); - } - }; - }) - .ok(); - }) - .detach(); - } - }), - ); - subscriptions.push( // Observe all events emitted by the chat registry cx.subscribe_in(&chat, window, move |this, chat, ev, window, cx| { @@ -171,6 +110,11 @@ impl ChatSpace { }), ); + // Set the default layout for app's dock + cx.defer_in(window, |this, window, cx| { + this.set_layout(window, cx); + }); + Self { dock, title_bar, @@ -179,43 +123,29 @@ impl ChatSpace { } } - fn set_onboarding_layout(&mut self, window: &mut Window, cx: &mut Context) { - let panel = Arc::new(onboarding::init(window, cx)); - let center = DockItem::panel(panel); - - self.dock.update(cx, |this, cx| { - this.reset(window, cx); - this.set_center(center, window, cx); - }); - } - - fn set_startup_layout(&mut self, cre: Credential, window: &mut Window, cx: &mut Context) { - let panel = Arc::new(startup::init(cre, window, cx)); - let center = DockItem::panel(panel); - - self.dock.update(cx, |this, cx| { - this.reset(window, cx); - this.set_center(center, window, cx); - }); - } - - fn set_default_layout(&mut self, window: &mut Window, cx: &mut Context) { + fn set_layout(&mut self, window: &mut Window, cx: &mut Context) { let weak_dock = self.dock.downgrade(); - let sidebar = Arc::new(sidebar::init(window, cx)); - let center = Arc::new(welcome::init(window, cx)); + // Sidebar + let left = DockItem::panel(Arc::new(sidebar::init(window, cx))); - let left = DockItem::panel(sidebar); + // Main workspace let center = DockItem::split_with_sizes( Axis::Vertical, - vec![DockItem::tabs(vec![center], None, &weak_dock, window, cx)], + vec![DockItem::tabs( + vec![Arc::new(welcome::init(window, cx))], + None, + &weak_dock, + window, + cx, + )], vec![None], &weak_dock, window, cx, ); - self.ready = true; + // Update the dock layout self.dock.update(cx, |this, cx| { this.set_left_dock(left, Some(px(DEFAULT_SIDEBAR_WIDTH)), true, window, cx); this.set_center(center, window, cx); @@ -426,24 +356,6 @@ impl ChatSpace { Some(ids) } - fn set_center_panel

(panel: P, window: &mut Window, cx: &mut App) - where - P: PanelView, - { - if let Some(Some(root)) = window.root::() { - if let Ok(chatspace) = root.read(cx).view().clone().downcast::() { - let panel = Arc::new(panel); - let center = DockItem::panel(panel); - - chatspace.update(cx, |this, cx| { - this.dock.update(cx, |this, cx| { - this.set_center(center, window, cx); - }); - }); - } - } - } - fn titlebar_left(&mut self, _window: &mut Window, cx: &Context) -> impl IntoElement { let nostr = NostrRegistry::global(cx); let chat = ChatRegistry::global(cx); @@ -569,62 +481,18 @@ impl ChatSpace { ) }) } - - fn titlebar_center(&mut self, cx: &mut Context) -> impl IntoElement { - let entity = cx.entity().downgrade(); - let panel = self.dock.read(cx).items.view(); - let title = panel.title(cx); - let id = panel.panel_id(cx); - - if id == "Onboarding" { - return div(); - }; - - h_flex() - .flex_1() - .w_full() - .justify_center() - .text_center() - .font_semibold() - .text_sm() - .child( - div().flex_1().child( - Button::new("back") - .icon(IconName::ArrowLeft) - .small() - .ghost_alt() - .rounded() - .on_click(move |_ev, window, cx| { - entity - .update(cx, |this, cx| { - this.set_onboarding_layout(window, cx); - }) - .expect("Entity has been released"); - }), - ), - ) - .child(div().flex_1().child(title)) - .child(div().flex_1()) - } } impl Render for ChatSpace { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let modal_layer = Root::render_modal_layer(window, cx); let notification_layer = Root::render_notification_layer(window, cx); - let left = self.titlebar_left(window, cx).into_any_element(); let right = self.titlebar_right(window, cx).into_any_element(); - let center = self.titlebar_center(cx).into_any_element(); - let single_panel = self.dock.read(cx).items.panel_ids(cx).is_empty(); // Update title bar children self.title_bar.update(cx, |this, _cx| { - if single_panel { - this.set_children(vec![center]); - } else { - this.set_children(vec![left, right]); - } + this.set_children(vec![left, right]); }); div() diff --git a/crates/coop/src/login/mod.rs b/crates/coop/src/login/mod.rs index b93ad3c..a62e42e 100644 --- a/crates/coop/src/login/mod.rs +++ b/crates/coop/src/login/mod.rs @@ -18,8 +18,6 @@ use ui::input::{InputEvent, InputState, TextInput}; use ui::notification::Notification; use ui::{v_flex, Disableable, StyledExt, WindowExtension}; -use crate::actions::CoopAuthUrlHandler; - pub fn init(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Login::new(window, cx)) } @@ -120,7 +118,7 @@ impl Login { let mut signer = NostrConnect::new(uri, app_keys.clone(), timeout, None).unwrap(); // Handle auth url with the default browser - signer.auth_url_handler(CoopAuthUrlHandler); + // signer.auth_url_handler(CoopAuthUrlHandler); // Start countdown cx.spawn_in(window, async move |this, cx| { diff --git a/crates/coop/src/views/mod.rs b/crates/coop/src/views/mod.rs index 2e6c806..2b146b7 100644 --- a/crates/coop/src/views/mod.rs +++ b/crates/coop/src/views/mod.rs @@ -1,5 +1,4 @@ pub mod compose; -pub mod onboarding; pub mod preferences; pub mod screening; pub mod setup_relay; diff --git a/crates/coop/src/views/onboarding.rs b/crates/coop/src/views/onboarding.rs deleted file mode 100644 index ba29e34..0000000 --- a/crates/coop/src/views/onboarding.rs +++ /dev/null @@ -1,363 +0,0 @@ -use std::sync::Arc; -use std::time::Duration; - -use common::{TextUtils, CLIENT_NAME, NOSTR_CONNECT_RELAY, NOSTR_CONNECT_TIMEOUT}; -use gpui::prelude::FluentBuilder; -use gpui::{ - div, img, px, relative, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter, - FocusHandle, Focusable, Image, InteractiveElement, IntoElement, ParentElement, Render, - SharedString, StatefulInteractiveElement, Styled, Task, Window, -}; -use key_store::{KeyItem, KeyStore}; -use nostr_connect::prelude::*; -use smallvec::{smallvec, SmallVec}; -use state::NostrRegistry; -use theme::ActiveTheme; -use ui::button::{Button, ButtonVariants}; -use ui::dock_area::panel::{Panel, PanelEvent}; -use ui::notification::Notification; -use ui::{divider, h_flex, v_flex, Icon, IconName, Sizable, StyledExt, WindowExtension}; - -use crate::chatspace::{self}; - -pub fn init(window: &mut Window, cx: &mut App) -> Entity { - Onboarding::new(window, cx) -} - -#[derive(Debug, Clone)] -pub enum NostrConnectApp { - Nsec(String), - Amber(String), - Aegis(String), -} - -impl NostrConnectApp { - pub fn all() -> Vec { - vec![ - NostrConnectApp::Nsec("https://nsec.app".to_string()), - NostrConnectApp::Amber("https://github.com/greenart7c3/Amber".to_string()), - NostrConnectApp::Aegis("https://github.com/ZharlieW/Aegis".to_string()), - ] - } - - pub fn url(&self) -> &str { - match self { - Self::Nsec(url) | Self::Amber(url) | Self::Aegis(url) => url, - } - } - - pub fn as_str(&self) -> String { - match self { - NostrConnectApp::Nsec(_) => "nsec.app (Desktop)".into(), - NostrConnectApp::Amber(_) => "Amber (Android)".into(), - NostrConnectApp::Aegis(_) => "Aegis (iOS)".into(), - } - } -} - -pub struct Onboarding { - app_keys: Keys, - qr_code: Option>, - - /// Panel - name: SharedString, - focus_handle: FocusHandle, - - /// Background tasks - _tasks: SmallVec<[Task<()>; 1]>, -} - -impl Onboarding { - pub fn new(window: &mut Window, cx: &mut App) -> Entity { - cx.new(|cx| Self::view(window, cx)) - } - - fn view(window: &mut Window, cx: &mut Context) -> Self { - let app_keys = Keys::generate(); - let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT); - - let relay = RelayUrl::parse(NOSTR_CONNECT_RELAY).unwrap(); - let uri = NostrConnectUri::client(app_keys.public_key(), vec![relay], CLIENT_NAME); - let qr_code = uri.to_string().to_qr(); - - // NIP46: https://github.com/nostr-protocol/nips/blob/master/46.md - // - // Direct connection initiated by the client - let signer = NostrConnect::new(uri, app_keys.clone(), timeout, None).unwrap(); - - let mut tasks = smallvec![]; - - tasks.push( - // Wait for nostr connect - cx.spawn_in(window, async move |this, cx| { - let result = signer.bunker_uri().await; - - this.update_in(cx, |this, window, cx| { - match result { - Ok(uri) => { - this.save_connection(&uri, window, cx); - this.connect(signer, cx); - } - Err(e) => { - window.push_notification(Notification::error(e.to_string()), cx); - } - }; - }) - .ok(); - }), - ); - - Self { - qr_code, - app_keys, - name: "Onboarding".into(), - focus_handle: cx.focus_handle(), - _tasks: tasks, - } - } - - fn save_connection( - &mut self, - uri: &NostrConnectUri, - window: &mut Window, - cx: &mut Context, - ) { - let keystore = KeyStore::global(cx).read(cx).backend(); - let username = self.app_keys.public_key().to_hex(); - let secret = self.app_keys.secret_key().to_secret_bytes(); - let mut clean_uri = uri.to_string(); - - // Clear the secret parameter in the URI if it exists - if let Some(s) = uri.secret() { - clean_uri = clean_uri.replace(s, ""); - } - - cx.spawn_in(window, async move |this, cx| { - let user_url = KeyItem::User.to_string(); - let bunker_url = KeyItem::Bunker.to_string(); - let user_password = clean_uri.into_bytes(); - - // Write bunker uri to keyring for further connection - if let Err(e) = keystore - .write_credentials(&user_url, "bunker", &user_password, cx) - .await - { - this.update_in(cx, |_, window, cx| { - window.push_notification(e.to_string(), cx); - }) - .ok(); - } - - // Write the app keys for further connection - if let Err(e) = keystore - .write_credentials(&bunker_url, &username, &secret, cx) - .await - { - this.update_in(cx, |_, window, cx| { - window.push_notification(e.to_string(), cx); - }) - .ok(); - } - }) - .detach(); - } - - fn connect(&mut self, signer: NostrConnect, cx: &mut Context) { - let nostr = NostrRegistry::global(cx); - let client = nostr.read(cx).client(); - - cx.background_spawn(async move { - client.set_signer(signer).await; - }) - .detach(); - } - - fn render_apps(&self, cx: &Context) -> impl IntoIterator { - let all_apps = NostrConnectApp::all(); - let mut items = Vec::with_capacity(all_apps.len()); - - for (ix, item) in all_apps.into_iter().enumerate() { - items.push(self.render_app(ix, item.as_str(), item.url(), cx)); - } - - items - } - - fn render_app(&self, ix: usize, label: T, url: &str, cx: &Context) -> impl IntoElement - where - T: Into, - { - div() - .id(ix) - .flex_1() - .rounded(cx.theme().radius) - .py_0p5() - .px_2() - .bg(cx.theme().ghost_element_background_alt) - .child(label.into()) - .on_click({ - let url = url.to_owned(); - move |_e, _window, cx| { - cx.open_url(&url); - } - }) - } -} - -impl Panel for Onboarding { - fn panel_id(&self) -> SharedString { - self.name.clone() - } - - fn title(&self, _cx: &App) -> AnyElement { - self.name.clone().into_any_element() - } -} - -impl EventEmitter for Onboarding {} - -impl Focusable for Onboarding { - fn focus_handle(&self, _: &App) -> gpui::FocusHandle { - self.focus_handle.clone() - } -} - -impl Render for Onboarding { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - h_flex() - .size_full() - .child( - v_flex() - .flex_1() - .h_full() - .gap_10() - .items_center() - .justify_center() - .child( - v_flex() - .items_center() - .justify_center() - .gap_4() - .child( - svg() - .path("brand/coop.svg") - .size_16() - .text_color(cx.theme().elevated_surface_background), - ) - .child( - div() - .text_center() - .child( - div() - .text_xl() - .font_semibold() - .line_height(relative(1.3)) - .child(SharedString::from("Welcome to Coop")), - ) - .child(div().text_color(cx.theme().text_muted).child( - SharedString::from("Chat Freely, Stay Private on Nostr."), - )), - ), - ) - .child( - v_flex() - .w_80() - .gap_3() - .child( - Button::new("continue_btn") - .icon(Icon::new(IconName::ArrowRight)) - .label(SharedString::from("Start Messaging on Nostr")) - .primary() - .large() - .bold() - .reverse() - .on_click(cx.listener(move |_, _, window, cx| { - chatspace::new_account(window, cx); - })), - ) - .child( - h_flex() - .my_1() - .gap_1() - .child(divider(cx)) - .child(div().text_sm().text_color(cx.theme().text_muted).child( - SharedString::from( - "Already have an account? Continue with", - ), - )) - .child(divider(cx)), - ) - .child( - Button::new("key") - .label("Secret Key or Bunker") - .large() - .ghost_alt() - .on_click(cx.listener(move |_, _, window, cx| { - chatspace::login(window, cx); - })), - ), - ), - ) - .child( - div() - .relative() - .p_2() - .flex_1() - .h_full() - .rounded(cx.theme().radius_lg) - .child( - v_flex() - .size_full() - .justify_center() - .bg(cx.theme().surface_background) - .rounded(cx.theme().radius_lg) - .child( - v_flex() - .gap_5() - .items_center() - .justify_center() - .when_some(self.qr_code.as_ref(), |this, qr| { - this.child( - img(qr.clone()) - .size(px(256.)) - .rounded(cx.theme().radius_lg) - .when(cx.theme().shadow, |this| this.shadow_lg()) - .border_1() - .border_color(cx.theme().element_active), - ) - }) - .child( - v_flex() - .justify_center() - .items_center() - .text_center() - .child( - div() - .font_semibold() - .line_height(relative(1.3)) - .child(SharedString::from( - "Continue with Nostr Connect", - )), - ) - .child( - div() - .text_sm() - .text_color(cx.theme().text_muted) - .child(SharedString::from( - "Use Nostr Connect apps to scan the code", - )), - ) - .child( - h_flex() - .mt_2() - .gap_1() - .text_xs() - .justify_center() - .children(self.render_apps(cx)), - ), - ), - ), - ), - ) - } -} diff --git a/crates/coop/src/views/startup.rs b/crates/coop/src/views/startup.rs index 4a77d82..80b14de 100644 --- a/crates/coop/src/views/startup.rs +++ b/crates/coop/src/views/startup.rs @@ -20,7 +20,7 @@ use ui::dock_area::panel::{Panel, PanelEvent}; use ui::indicator::Indicator; use ui::{h_flex, v_flex, Sizable, StyledExt, WindowExtension}; -use crate::actions::{reset, CoopAuthUrlHandler}; +use crate::actions::reset; pub fn init(cre: Credential, window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Startup::new(cre, window, cx)) @@ -129,7 +129,7 @@ impl Startup { let mut signer = NostrConnect::new(uri, keys, timeout, None).unwrap(); // Handle auth url with the default browser - signer.auth_url_handler(CoopAuthUrlHandler); + // signer.auth_url_handler(CoopAuthUrlHandler); // Connect to the remote signer this._tasks.push( diff --git a/crates/state/Cargo.toml b/crates/state/Cargo.toml index 51ac44e..64dcec8 100644 --- a/crates/state/Cargo.toml +++ b/crates/state/Cargo.toml @@ -9,11 +9,13 @@ common = { path = "../common" } nostr-sdk.workspace = true nostr-lmdb.workspace = true +nostr-connect.workspace = true gpui.workspace = true smol.workspace = true flume.workspace = true log.workspace = true anyhow.workspace = true +webbrowser.workspace = true rustls = "0.23" diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs index 201027c..4030f5a 100644 --- a/crates/state/src/lib.rs +++ b/crates/state/src/lib.rs @@ -2,8 +2,9 @@ use std::collections::HashSet; use std::time::Duration; use anyhow::Error; -use common::{config_dir, BOOTSTRAP_RELAYS, SEARCH_RELAYS}; +use common::{config_dir, BOOTSTRAP_RELAYS, CLIENT_NAME, SEARCH_RELAYS}; use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task}; +use nostr_connect::prelude::*; use nostr_lmdb::NostrLmdb; use nostr_sdk::prelude::*; @@ -25,6 +26,10 @@ pub fn init(cx: &mut App) { /// Default timeout for subscription pub const TIMEOUT: u64 = 3; +/// Default timeout for Nostr Connect +pub const NOSTR_CONNECT_TIMEOUT: u64 = 200; +/// Default Nostr Connect relay +pub const NOSTR_CONNECT_RELAY: &str = "wss://relay.nsec.app"; /// Default subscription id for gift wrap events pub const GIFTWRAP_SUBSCRIPTION: &str = "giftwrap-events"; @@ -592,4 +597,56 @@ impl NostrRegistry { Ok(()) })); } + + /// Store a connection for future uses + pub fn persit_connection(&mut self, uri: NostrConnectUri, cx: &mut App) { + let client = self.client(); + let rng_keys = Keys::generate(); + + self.tasks.push(cx.background_spawn(async move { + // Construct the event for application-specific data + let event = EventBuilder::new(Kind::ApplicationSpecificData, uri.to_string()) + .tag(Tag::identifier("coop:account")) + .sign(&rng_keys) + .await?; + + // Store the event in the database + client.database().save_event(&event).await?; + + Ok(()) + })); + } + + /// Generate a direct nostr connection initiated by the client + pub fn client_connect(&self, relay: Option) -> (NostrConnect, NostrConnectUri) { + let app_keys = self.app_keys(); + let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT); + + // Determine the relay will be used for Nostr Connect + let relay = match relay { + Some(relay) => relay, + None => RelayUrl::parse(NOSTR_CONNECT_RELAY).unwrap(), + }; + + // Generate the nostr connect uri + let uri = NostrConnectUri::client(app_keys.public_key(), vec![relay], CLIENT_NAME); + + // Generate the nostr connect + let signer = NostrConnect::new(uri.clone(), app_keys.clone(), timeout, None).unwrap(); + + (signer, uri) + } +} + +#[derive(Debug, Clone)] +pub struct CoopAuthUrlHandler; + +impl AuthUrlHandler for CoopAuthUrlHandler { + #[allow(mismatched_lifetime_syntaxes)] + fn on_auth_url(&self, auth_url: Url) -> BoxedFuture> { + Box::pin(async move { + webbrowser::open(auth_url.as_str())?; + Ok(()) + }) + } } -- 2.49.1 From 311af51beecaef9140fb95c82cd2a24596e2b28f Mon Sep 17 00:00:00 2001 From: reya Date: Tue, 20 Jan 2026 18:35:20 +0700 Subject: [PATCH 08/30] wip --- crates/coop/src/main.rs | 4 +- .../coop/src/{chatspace.rs => workspace.rs} | 37 ++++++++++++------- 2 files changed, 26 insertions(+), 15 deletions(-) rename crates/coop/src/{chatspace.rs => workspace.rs} (96%) diff --git a/crates/coop/src/main.rs b/crates/coop/src/main.rs index 6fe4ec0..903d912 100644 --- a/crates/coop/src/main.rs +++ b/crates/coop/src/main.rs @@ -12,12 +12,12 @@ use ui::Root; use crate::actions::Quit; mod actions; -mod chatspace; mod login; mod new_identity; mod sidebar; mod user; mod views; +mod workspace; fn main() { // Initialize logging @@ -111,7 +111,7 @@ fn main() { auto_update::init(cx); // Root Entity - Root::new(chatspace::init(window, cx).into(), window, cx) + Root::new(workspace::init(window, cx).into(), window, cx) }) }) .expect("Failed to open window. Please restart the application."); diff --git a/crates/coop/src/chatspace.rs b/crates/coop/src/workspace.rs similarity index 96% rename from crates/coop/src/chatspace.rs rename to crates/coop/src/workspace.rs index 6ae1e2b..fea0fc7 100644 --- a/crates/coop/src/chatspace.rs +++ b/crates/coop/src/workspace.rs @@ -31,30 +31,30 @@ use crate::actions::{ use crate::user::viewer; use crate::views::compose::compose_button; use crate::views::{preferences, setup_relay, welcome}; -use crate::{login, sidebar, user}; +use crate::{sidebar, user}; -pub fn init(window: &mut Window, cx: &mut App) -> Entity { - cx.new(|cx| ChatSpace::new(window, cx)) +pub fn init(window: &mut Window, cx: &mut App) -> Entity { + cx.new(|cx| Workspace::new(window, cx)) } #[derive(Debug)] -pub struct ChatSpace { +pub struct Workspace { /// App's Title Bar title_bar: Entity, /// App's Dock Area dock: Entity, - /// Determines if the chat space is ready to use - ready: bool, - /// Event subscriptions - _subscriptions: SmallVec<[Subscription; 3]>, + _subscriptions: SmallVec<[Subscription; 4]>, } -impl ChatSpace { +impl Workspace { fn new(window: &mut Window, cx: &mut Context) -> Self { let chat = ChatRegistry::global(cx); + let nostr = NostrRegistry::global(cx); + let identity = nostr.read(cx).identity(); + let title_bar = cx.new(|_| TitleBar::new()); let dock = cx.new(|cx| DockArea::new(window, cx)); @@ -67,6 +67,17 @@ impl ChatSpace { }), ); + subscriptions.push( + // Observe the identity entity + cx.observe_in(&identity, window, move |this, state, window, cx| { + if state.read(cx).has_public_key() { + this.dock.update(cx, |this, cx| { + this.toggle_dock(DockPlacement::Left, window, cx); + }); + }; + }), + ); + subscriptions.push( // Observe all events emitted by the chat registry cx.subscribe_in(&chat, window, move |this, chat, ev, window, cx| { @@ -87,6 +98,7 @@ impl ChatSpace { this.dock.update(cx, |this, cx| { // Force focus to the tab panel this.focus_tab_panel(window, cx); + // Dispatch the close panel action cx.defer_in(window, |_, window, cx| { window.dispatch_action(Box::new(ClosePanel), cx); @@ -118,7 +130,6 @@ impl ChatSpace { Self { dock, title_bar, - ready: false, _subscriptions: subscriptions, } } @@ -147,7 +158,7 @@ impl ChatSpace { // Update the dock layout self.dock.update(cx, |this, cx| { - this.set_left_dock(left, Some(px(DEFAULT_SIDEBAR_WIDTH)), true, window, cx); + this.set_left_dock(left, Some(px(DEFAULT_SIDEBAR_WIDTH)), false, window, cx); this.set_center(center, window, cx); }); } @@ -483,7 +494,7 @@ impl ChatSpace { } } -impl Render for ChatSpace { +impl Render for Workspace { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let modal_layer = Root::render_modal_layer(window, cx); let notification_layer = Root::render_notification_layer(window, cx); @@ -496,7 +507,7 @@ impl Render for ChatSpace { }); div() - .id(SharedString::from("chatspace")) + .id(SharedString::from("workspace")) .on_action(cx.listener(Self::on_settings)) .on_action(cx.listener(Self::on_profile)) .on_action(cx.listener(Self::on_relays)) -- 2.49.1 From dd6b93bd79a0e17996afd772352a54f4a47cb3a3 Mon Sep 17 00:00:00 2001 From: reya Date: Wed, 21 Jan 2026 12:04:23 +0700 Subject: [PATCH 09/30] wip --- crates/coop/src/main.rs | 2 - crates/coop/src/views/mod.rs | 1 - crates/coop/src/views/welcome.rs | 26 +++---- crates/state/src/identity.rs | 9 +++ crates/state/src/lib.rs | 123 +++++++++++++++++++++++++++++-- 5 files changed, 139 insertions(+), 22 deletions(-) diff --git a/crates/coop/src/main.rs b/crates/coop/src/main.rs index 903d912..34ddd96 100644 --- a/crates/coop/src/main.rs +++ b/crates/coop/src/main.rs @@ -12,8 +12,6 @@ use ui::Root; use crate::actions::Quit; mod actions; -mod login; -mod new_identity; mod sidebar; mod user; mod views; diff --git a/crates/coop/src/views/mod.rs b/crates/coop/src/views/mod.rs index 2b146b7..e615adf 100644 --- a/crates/coop/src/views/mod.rs +++ b/crates/coop/src/views/mod.rs @@ -2,5 +2,4 @@ pub mod compose; pub mod preferences; pub mod screening; pub mod setup_relay; -pub mod startup; pub mod welcome; diff --git a/crates/coop/src/views/welcome.rs b/crates/coop/src/views/welcome.rs index ff5aff9..f21f27d 100644 --- a/crates/coop/src/views/welcome.rs +++ b/crates/coop/src/views/welcome.rs @@ -5,28 +5,20 @@ use gpui::{ }; use theme::ActiveTheme; use ui::dock_area::panel::{Panel, PanelEvent}; -use ui::{v_flex, StyledExt}; +use ui::{h_flex, v_flex, StyledExt}; pub fn init(window: &mut Window, cx: &mut App) -> Entity { - Welcome::new(window, cx) + cx.new(|cx| Welcome::new(window, cx)) } pub struct Welcome { name: SharedString, - version: SharedString, focus_handle: FocusHandle, } impl Welcome { - pub fn new(window: &mut Window, cx: &mut App) -> Entity { - cx.new(|cx| Self::view(window, cx)) - } - - fn view(_window: &mut Window, cx: &mut Context) -> Self { - let version = SharedString::from(format!("Version: {}", env!("CARGO_PKG_VERSION"))); - + fn new(_window: &mut Window, cx: &mut App) -> Self { Self { - version, name: "Welcome".into(), focus_handle: cx.focus_handle(), } @@ -39,12 +31,19 @@ impl Panel for Welcome { } fn title(&self, cx: &App) -> AnyElement { - div() + h_flex() + .gap_1p5() .child( svg() .path("brand/coop.svg") .size_4() - .text_color(cx.theme().element_background), + .text_color(cx.theme().text_muted), + ) + .child( + div() + .text_sm() + .text_color(cx.theme().text_muted) + .child(self.name.clone()), ) .into_any_element() } @@ -92,7 +91,6 @@ impl Render for Welcome { .id("version") .text_color(cx.theme().text_placeholder) .text_xs() - .child(self.version.clone()) .on_click(|_, _window, cx| { cx.open_url("https://github.com/lumehq/coop/releases"); }), diff --git a/crates/state/src/identity.rs b/crates/state/src/identity.rs index 8c59c18..418e836 100644 --- a/crates/state/src/identity.rs +++ b/crates/state/src/identity.rs @@ -20,6 +20,9 @@ pub struct Identity { /// The public key of the account pub public_key: Option, + /// Whether the identity is owned by the user + pub owned: bool, + /// Status of the current user NIP-65 relays relay_list: RelayState, @@ -37,6 +40,7 @@ impl Identity { pub fn new() -> Self { Self { public_key: None, + owned: false, relay_list: RelayState::default(), messaging_relays: RelayState::default(), } @@ -83,4 +87,9 @@ impl Identity { pub fn unset_public_key(&mut self) { self.public_key = None; } + + /// Sets whether the identity is owned by the user. + pub fn set_owned(&mut self, owned: bool) { + self.owned = owned; + } } diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs index 4030f5a..a83e5de 100644 --- a/crates/state/src/lib.rs +++ b/crates/state/src/lib.rs @@ -1,7 +1,8 @@ use std::collections::HashSet; +use std::os::unix::fs::PermissionsExt; use std::time::Duration; -use anyhow::Error; +use anyhow::{anyhow, Error}; use common::{config_dir, BOOTSTRAP_RELAYS, CLIENT_NAME, SEARCH_RELAYS}; use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task}; use nostr_connect::prelude::*; @@ -173,6 +174,14 @@ impl NostrRegistry { }), ); + cx.defer(|cx| { + let nostr = NostrRegistry::global(cx); + + nostr.update(cx, |this, cx| { + this.get_identity(cx); + }); + }); + Self { client, app_keys, @@ -305,9 +314,15 @@ impl NostrRegistry { let keys = Keys::generate(); let secret_key = keys.secret_key(); + // Create directory and write secret key std::fs::create_dir_all(dir.parent().unwrap())?; std::fs::write(&dir, secret_key.to_secret_bytes())?; + // Set permissions to readonly + let mut perms = std::fs::metadata(&dir)?.permissions(); + perms.set_mode(0o400); + std::fs::set_permissions(&dir, perms)?; + return Ok(keys); } }; @@ -390,7 +405,7 @@ impl NostrRegistry { } /// Set the signer for the nostr client and verify the public key - pub fn set_signer(&mut self, signer: T, cx: &mut Context) + pub fn set_signer(&mut self, signer: T, owned: bool, cx: &mut Context) where T: NostrSigner + 'static, { @@ -414,6 +429,7 @@ impl NostrRegistry { Ok(public_key) => { identity.update(cx, |this, cx| { this.set_public_key(public_key); + this.set_owned(owned); cx.notify(); })?; } @@ -598,8 +614,102 @@ impl NostrRegistry { })); } - /// Store a connection for future uses - pub fn persit_connection(&mut self, uri: NostrConnectUri, cx: &mut App) { + /// Get local stored identity + fn get_identity(&mut self, cx: &mut Context) { + let read_credential = cx.read_credentials(CLIENT_NAME); + + self.tasks.push(cx.spawn(async move |this, cx| { + match read_credential.await { + Ok(Some((_, secret))) => { + let secret = SecretKey::from_slice(&secret)?; + let keys = Keys::new(secret); + + this.update(cx, |this, cx| { + this.set_signer(keys, false, cx); + }) + .ok(); + } + _ => { + this.update(cx, |this, cx| { + this.get_bunker(cx); + }) + .ok(); + } + } + + Ok(()) + })); + } + + /// Create a new identity + fn create_identity(&mut self, cx: &mut Context) { + let keys = Keys::generate(); + let write_credential = cx.write_credentials( + CLIENT_NAME, + &keys.public_key().to_hex(), + &keys.secret_key().to_secret_bytes(), + ); + + // Update the signer + self.set_signer(keys, false, cx); + + // Spawn a task to write the credentials + cx.background_spawn(async move { + if let Err(e) = write_credential.await { + log::error!("Failed to write credentials: {}", e); + } + }) + .detach(); + } + + /// Get local stored bunker connection + fn get_bunker(&mut self, cx: &mut Context) { + let client = self.client(); + let app_keys = self.app_keys().clone(); + let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT); + + let task: Task> = cx.background_spawn(async move { + log::info!("Getting bunker connection"); + + let filter = Filter::new() + .kind(Kind::ApplicationSpecificData) + .identifier("coop:account") + .limit(1); + + if let Some(event) = client.database().query(filter).await?.first_owned() { + let uri = NostrConnectUri::parse(event.content)?; + let signer = NostrConnect::new(uri.clone(), app_keys.clone(), timeout, None)?; + + Ok(signer) + } else { + Err(anyhow!("No account found")) + } + }); + + self.tasks.push(cx.spawn(async move |this, cx| { + match task.await { + Ok(signer) => { + this.update(cx, |this, cx| { + this.set_signer(signer, true, cx); + }) + .ok(); + } + Err(e) => { + log::warn!("Failed to get bunker: {e}"); + // Create a new identity if no stored bunker exists + this.update(cx, |this, cx| { + this.create_identity(cx); + }) + .ok(); + } + } + + Ok(()) + })); + } + + /// Store the bunker connection for the next login + pub fn persist_bunker(&mut self, uri: NostrConnectUri, cx: &mut App) { let client = self.client(); let rng_keys = Keys::generate(); @@ -632,7 +742,10 @@ impl NostrRegistry { let uri = NostrConnectUri::client(app_keys.public_key(), vec![relay], CLIENT_NAME); // Generate the nostr connect - let signer = NostrConnect::new(uri.clone(), app_keys.clone(), timeout, None).unwrap(); + let mut signer = NostrConnect::new(uri.clone(), app_keys.clone(), timeout, None).unwrap(); + + // Handle the auth request + signer.auth_url_handler(CoopAuthUrlHandler); (signer, uri) } -- 2.49.1 From 2fa4436e2dbe4991e24dd3b46f8fc086ebb546ad Mon Sep 17 00:00:00 2001 From: reya Date: Thu, 22 Jan 2026 06:50:57 +0700 Subject: [PATCH 10/30] move dock to seperated crate --- Cargo.lock | 18 ++- crates/chat_ui/Cargo.toml | 1 + crates/chat_ui/src/lib.rs | 2 +- crates/coop/Cargo.toml | 1 + crates/coop/src/login/mod.rs | 2 +- crates/coop/src/new_identity/mod.rs | 2 +- crates/coop/src/sidebar/mod.rs | 2 +- crates/coop/src/views/startup.rs | 2 +- crates/coop/src/views/welcome.rs | 2 +- crates/coop/src/workspace.rs | 133 +++++++----------- crates/dock/Cargo.toml | 15 ++ crates/{ui/src/dock_area => dock/src}/dock.rs | 22 +-- .../src/dock_area/mod.rs => dock/src/lib.rs} | 18 +-- .../{ui/src/dock_area => dock/src}/panel.rs | 5 +- .../src/dock_area => dock/src}/stack_panel.rs | 12 +- .../src/dock_area => dock/src}/tab_panel.rs | 22 ++- crates/state/src/lib.rs | 2 + crates/title_bar/Cargo.toml | 3 - crates/title_bar/src/lib.rs | 2 +- crates/title_bar/src/platforms/linux.rs | 13 +- crates/ui/src/lib.rs | 1 - crates/ui/src/resizable/mod.rs | 2 +- crates/ui/src/resizable/panel.rs | 10 +- crates/ui/src/resizable/resize_handle.rs | 8 +- 24 files changed, 138 insertions(+), 162 deletions(-) create mode 100644 crates/dock/Cargo.toml rename crates/{ui/src/dock_area => dock/src}/dock.rs (96%) rename crates/{ui/src/dock_area/mod.rs => dock/src/lib.rs} (98%) rename crates/{ui/src/dock_area => dock/src}/panel.rs (98%) rename crates/{ui/src/dock_area => dock/src}/stack_panel.rs (98%) rename crates/{ui/src/dock_area => dock/src}/tab_panel.rs (97%) diff --git a/Cargo.lock b/Cargo.lock index 6da7c4c..02074c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1034,6 +1034,7 @@ dependencies = [ "anyhow", "chat", "common", + "dock", "emojis", "gpui", "gpui_tokio", @@ -1295,6 +1296,7 @@ dependencies = [ "chat_ui", "common", "device", + "dock", "futures", "gpui", "gpui_tokio", @@ -1740,6 +1742,19 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "dock" +version = "0.3.0" +dependencies = [ + "anyhow", + "common", + "gpui", + "log", + "smallvec", + "theme", + "ui", +] + [[package]] name = "downcast-rs" version = "1.2.1" @@ -6584,10 +6599,7 @@ dependencies = [ "common", "gpui", "linicon", - "log", - "nostr-sdk", "smallvec", - "smol", "theme", "ui", "windows 0.61.3", diff --git a/crates/chat_ui/Cargo.toml b/crates/chat_ui/Cargo.toml index bff3e8d..9f99fc0 100644 --- a/crates/chat_ui/Cargo.toml +++ b/crates/chat_ui/Cargo.toml @@ -7,6 +7,7 @@ publish.workspace = true [dependencies] state = { path = "../state" } ui = { path = "../ui" } +dock = { path = "../dock" } theme = { path = "../theme" } common = { path = "../common" } person = { path = "../person" } diff --git a/crates/chat_ui/src/lib.rs b/crates/chat_ui/src/lib.rs index 9bbd428..791beee 100644 --- a/crates/chat_ui/src/lib.rs +++ b/crates/chat_ui/src/lib.rs @@ -4,6 +4,7 @@ use std::time::Duration; pub use actions::*; use chat::{Message, RenderedMessage, Room, RoomEvent, RoomKind, SendReport}; use common::{nip96_upload, RenderedTimestamp}; +use dock::panel::{Panel, PanelEvent}; use gpui::prelude::FluentBuilder; use gpui::{ div, img, list, px, red, relative, rems, svg, white, AnyElement, App, AppContext, @@ -25,7 +26,6 @@ use theme::ActiveTheme; use ui::avatar::Avatar; use ui::button::{Button, ButtonVariants}; use ui::context_menu::ContextMenuExt; -use ui::dock_area::panel::{Panel, PanelEvent}; use ui::input::{InputEvent, InputState, TextInput}; use ui::notification::Notification; use ui::popup_menu::PopupMenuExt; diff --git a/crates/coop/Cargo.toml b/crates/coop/Cargo.toml index a5e5ca4..2147bc7 100644 --- a/crates/coop/Cargo.toml +++ b/crates/coop/Cargo.toml @@ -29,6 +29,7 @@ icons = [ [dependencies] assets = { path = "../assets" } ui = { path = "../ui" } +dock = { path = "../dock" } title_bar = { path = "../title_bar" } theme = { path = "../theme" } common = { path = "../common" } diff --git a/crates/coop/src/login/mod.rs b/crates/coop/src/login/mod.rs index a62e42e..39154bb 100644 --- a/crates/coop/src/login/mod.rs +++ b/crates/coop/src/login/mod.rs @@ -2,6 +2,7 @@ use std::time::Duration; use anyhow::anyhow; use common::BUNKER_TIMEOUT; +use dock::panel::{Panel, PanelEvent}; use gpui::prelude::FluentBuilder; use gpui::{ div, relative, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, @@ -13,7 +14,6 @@ use smallvec::{smallvec, SmallVec}; use state::NostrRegistry; use theme::ActiveTheme; use ui::button::{Button, ButtonVariants}; -use ui::dock_area::panel::{Panel, PanelEvent}; use ui::input::{InputEvent, InputState, TextInput}; use ui::notification::Notification; use ui::{v_flex, Disableable, StyledExt, WindowExtension}; diff --git a/crates/coop/src/new_identity/mod.rs b/crates/coop/src/new_identity/mod.rs index 89d7611..4c69475 100644 --- a/crates/coop/src/new_identity/mod.rs +++ b/crates/coop/src/new_identity/mod.rs @@ -1,5 +1,6 @@ use anyhow::{anyhow, Error}; use common::{default_nip17_relays, default_nip65_relays, nip96_upload, BOOTSTRAP_RELAYS}; +use dock::panel::{Panel, PanelEvent}; use gpui::{ rems, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement, PathPromptOptions, Render, SharedString, Styled, Task, Window, @@ -12,7 +13,6 @@ use smol::fs; use state::NostrRegistry; use ui::avatar::Avatar; use ui::button::{Button, ButtonVariants}; -use ui::dock_area::panel::{Panel, PanelEvent}; use ui::input::{InputState, TextInput}; use ui::modal::ModalButtonProps; use ui::{divider, v_flex, Disableable, IconName, Sizable, WindowExtension}; diff --git a/crates/coop/src/sidebar/mod.rs b/crates/coop/src/sidebar/mod.rs index a64842c..2cf5ef8 100644 --- a/crates/coop/src/sidebar/mod.rs +++ b/crates/coop/src/sidebar/mod.rs @@ -4,6 +4,7 @@ use std::time::Duration; use anyhow::{anyhow, Error}; use chat::{ChatEvent, ChatRegistry, Room, RoomKind}; use common::{DebouncedDelay, RenderedTimestamp, TextUtils, BOOTSTRAP_RELAYS, SEARCH_RELAYS}; +use dock::panel::{Panel, PanelEvent}; use gpui::prelude::FluentBuilder; use gpui::{ deferred, div, relative, uniform_list, App, AppContext, Context, Entity, EventEmitter, @@ -17,7 +18,6 @@ use smallvec::{smallvec, SmallVec}; use state::{NostrRegistry, GIFTWRAP_SUBSCRIPTION}; use theme::ActiveTheme; use ui::button::{Button, ButtonVariants}; -use ui::dock_area::panel::{Panel, PanelEvent}; use ui::input::{InputEvent, InputState, TextInput}; use ui::popup_menu::PopupMenuExt; use ui::{h_flex, v_flex, Icon, IconName, Selectable, Sizable, StyledExt, WindowExtension}; diff --git a/crates/coop/src/views/startup.rs b/crates/coop/src/views/startup.rs index 80b14de..b89d9ba 100644 --- a/crates/coop/src/views/startup.rs +++ b/crates/coop/src/views/startup.rs @@ -1,6 +1,7 @@ use std::time::Duration; use common::BUNKER_TIMEOUT; +use dock::panel::{Panel, PanelEvent}; use gpui::prelude::FluentBuilder; use gpui::{ div, relative, rems, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter, @@ -16,7 +17,6 @@ use state::NostrRegistry; use theme::ActiveTheme; use ui::avatar::Avatar; use ui::button::{Button, ButtonVariants}; -use ui::dock_area::panel::{Panel, PanelEvent}; use ui::indicator::Indicator; use ui::{h_flex, v_flex, Sizable, StyledExt, WindowExtension}; diff --git a/crates/coop/src/views/welcome.rs b/crates/coop/src/views/welcome.rs index f21f27d..f2acc1f 100644 --- a/crates/coop/src/views/welcome.rs +++ b/crates/coop/src/views/welcome.rs @@ -1,10 +1,10 @@ +use dock::panel::{Panel, PanelEvent}; use gpui::{ div, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, Window, }; use theme::ActiveTheme; -use ui::dock_area::panel::{Panel, PanelEvent}; use ui::{h_flex, v_flex, StyledExt}; pub fn init(window: &mut Window, cx: &mut App) -> Entity { diff --git a/crates/coop/src/workspace.rs b/crates/coop/src/workspace.rs index fea0fc7..b68d7a4 100644 --- a/crates/coop/src/workspace.rs +++ b/crates/coop/src/workspace.rs @@ -4,6 +4,8 @@ use auto_update::{AutoUpdateStatus, AutoUpdater}; use chat::{ChatEvent, ChatRegistry}; use chat_ui::{CopyPublicKey, OpenPublicKey}; use common::DEFAULT_SIDEBAR_WIDTH; +use dock::dock::DockPlacement; +use dock::{ClosePanel, DockArea, DockItem}; use gpui::prelude::FluentBuilder; use gpui::{ deferred, div, px, relative, rems, App, AppContext, Axis, ClipboardItem, Context, Entity, @@ -19,8 +21,6 @@ use theme::{ActiveTheme, Theme, ThemeMode, ThemeRegistry}; use title_bar::TitleBar; use ui::avatar::Avatar; use ui::button::{Button, ButtonVariants}; -use ui::dock_area::dock::DockPlacement; -use ui::dock_area::{ClosePanel, DockArea, DockItem}; use ui::modal::ModalButtonProps; use ui::popup_menu::PopupMenuExt; use ui::{h_flex, v_flex, IconName, Root, Sizable, WindowExtension}; @@ -46,15 +46,12 @@ pub struct Workspace { dock: Entity, /// Event subscriptions - _subscriptions: SmallVec<[Subscription; 4]>, + _subscriptions: SmallVec<[Subscription; 3]>, } impl Workspace { fn new(window: &mut Window, cx: &mut Context) -> Self { let chat = ChatRegistry::global(cx); - let nostr = NostrRegistry::global(cx); - let identity = nostr.read(cx).identity(); - let title_bar = cx.new(|_| TitleBar::new()); let dock = cx.new(|cx| DockArea::new(window, cx)); @@ -67,17 +64,6 @@ impl Workspace { }), ); - subscriptions.push( - // Observe the identity entity - cx.observe_in(&identity, window, move |this, state, window, cx| { - if state.read(cx).has_public_key() { - this.dock.update(cx, |this, cx| { - this.toggle_dock(DockPlacement::Left, window, cx); - }); - }; - }), - ); - subscriptions.push( // Observe all events emitted by the chat registry cx.subscribe_in(&chat, window, move |this, chat, ev, window, cx| { @@ -158,7 +144,7 @@ impl Workspace { // Update the dock layout self.dock.update(cx, |this, cx| { - this.set_left_dock(left, Some(px(DEFAULT_SIDEBAR_WIDTH)), false, window, cx); + this.set_left_dock(left, Some(px(DEFAULT_SIDEBAR_WIDTH)), true, window, cx); this.set_center(center, window, cx); }); } @@ -369,43 +355,50 @@ impl Workspace { fn titlebar_left(&mut self, _window: &mut Window, cx: &Context) -> impl IntoElement { let nostr = NostrRegistry::global(cx); - let chat = ChatRegistry::global(cx); - let status = chat.read(cx).loading(); - - if !nostr.read(cx).identity().read(cx).has_public_key() { - return div(); - } + let identity = nostr.read(cx).identity(); h_flex() .gap_2() .h_6() .w_full() - .child(compose_button()) - .when(status, |this| { - this.child(deferred( - h_flex() - .px_2() - .h_6() - .gap_1() - .text_xs() - .rounded_full() - .bg(cx.theme().surface_background) - .child(SharedString::from( - "Getting messages. This may take a while...", - )), - )) + .when_some(identity.read(cx).public_key, |this, public_key| { + let persons = PersonRegistry::global(cx); + let profile = persons.read(cx).get(&public_key, cx); + + this.child( + Button::new("user") + .small() + .reverse() + .transparent() + .icon(IconName::CaretDown) + .child(Avatar::new(profile.avatar()).size(rems(1.5))) + .popup_menu(move |this, _window, _cx| { + this.label(profile.name()) + .menu_with_icon("Profile", IconName::Emoji, Box::new(ViewProfile)) + .menu_with_icon( + "Messaging Relays", + IconName::Relay, + Box::new(ViewRelays), + ) + .separator() + .menu_with_icon("Dark Mode", IconName::Sun, Box::new(DarkMode)) + .menu_with_icon("Themes", IconName::Moon, Box::new(Themes)) + .menu_with_icon("Settings", IconName::Settings, Box::new(Settings)) + .menu_with_icon("Sign Out", IconName::Door, Box::new(Logout)) + }), + ) }) + .child(compose_button()) } fn titlebar_right(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let auto_update = AutoUpdater::global(cx); + let chat = ChatRegistry::global(cx); + let status = chat.read(cx).loading(); + let auto_update = AutoUpdater::global(cx); let relay_auth = RelayAuth::global(cx); let pending_requests = relay_auth.read(cx).pending_requests(cx); - let nostr = NostrRegistry::global(cx); - let identity = nostr.read(cx).identity(); - h_flex() .gap_2() .map(|this| match auto_update.read(cx).status.as_ref() { @@ -464,32 +457,19 @@ impl Workspace { }), ) }) - .when_some(identity.read(cx).public_key, |this, public_key| { - let persons = PersonRegistry::global(cx); - let profile = persons.read(cx).get(&public_key, cx); - - this.child( - Button::new("user") - .small() - .reverse() - .transparent() - .icon(IconName::CaretDown) - .child(Avatar::new(profile.avatar()).size(rems(1.45))) - .popup_menu(move |this, _window, _cx| { - this.label(profile.name()) - .menu_with_icon("Profile", IconName::Emoji, Box::new(ViewProfile)) - .menu_with_icon( - "Messaging Relays", - IconName::Relay, - Box::new(ViewRelays), - ) - .separator() - .menu_with_icon("Dark Mode", IconName::Sun, Box::new(DarkMode)) - .menu_with_icon("Themes", IconName::Moon, Box::new(Themes)) - .menu_with_icon("Settings", IconName::Settings, Box::new(Settings)) - .menu_with_icon("Sign Out", IconName::Door, Box::new(Logout)) - }), - ) + .when(status, |this| { + this.child(deferred( + h_flex() + .px_2() + .h_6() + .gap_1() + .text_xs() + .rounded_full() + .bg(cx.theme().surface_background) + .child(SharedString::from( + "Getting messages. This may take a while...", + )), + )) }) } } @@ -498,13 +478,6 @@ impl Render for Workspace { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let modal_layer = Root::render_modal_layer(window, cx); let notification_layer = Root::render_notification_layer(window, cx); - let left = self.titlebar_left(window, cx).into_any_element(); - let right = self.titlebar_right(window, cx).into_any_element(); - - // Update title bar children - self.title_bar.update(cx, |this, _cx| { - this.set_children(vec![left, right]); - }); div() .id(SharedString::from("workspace")) @@ -519,14 +492,8 @@ impl Render for Workspace { .on_action(cx.listener(Self::on_keyring)) .relative() .size_full() - .child( - v_flex() - .size_full() - // Title Bar - .child(self.title_bar.clone()) - // Dock - .child(self.dock.clone()), - ) + // Dock + .child(self.dock.clone()) // Notifications .children(notification_layer) // Modals diff --git a/crates/dock/Cargo.toml b/crates/dock/Cargo.toml new file mode 100644 index 0000000..36830b8 --- /dev/null +++ b/crates/dock/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "dock" +version.workspace = true +edition.workspace = true +publish.workspace = true + +[dependencies] +common = { path = "../common" } +theme = { path = "../theme" } +ui = { path = "../ui" } + +gpui.workspace = true +smallvec.workspace = true +anyhow.workspace = true +log.workspace = true diff --git a/crates/ui/src/dock_area/dock.rs b/crates/dock/src/dock.rs similarity index 96% rename from crates/ui/src/dock_area/dock.rs rename to crates/dock/src/dock.rs index 3c427a6..603510b 100644 --- a/crates/ui/src/dock_area/dock.rs +++ b/crates/dock/src/dock.rs @@ -6,27 +6,22 @@ use gpui::{ MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Point, Render, StatefulInteractiveElement, Style, Styled as _, WeakEntity, Window, }; -use serde::{Deserialize, Serialize}; use theme::ActiveTheme; +use ui::resizable::{HANDLE_PADDING, HANDLE_SIZE, PANEL_MIN_SIZE}; +use ui::{AxisExt as _, StyledExt}; use super::{DockArea, DockItem}; -use crate::dock_area::panel::PanelView; -use crate::dock_area::tab_panel::TabPanel; -use crate::resizable::{HANDLE_PADDING, HANDLE_SIZE, PANEL_MIN_SIZE}; -use crate::{AxisExt as _, StyledExt}; +use crate::panel::PanelView; +use crate::tab_panel::TabPanel; #[derive(Clone, Render)] struct ResizePanel; -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DockPlacement { - #[serde(rename = "center")] Center, - #[serde(rename = "left")] Left, - #[serde(rename = "bottom")] Bottom, - #[serde(rename = "right")] Right, } @@ -58,14 +53,19 @@ impl DockPlacement { pub struct Dock { pub(super) placement: DockPlacement, dock_area: WeakEntity, + + /// Dock layout pub(crate) panel: DockItem, + /// The size is means the width or height of the Dock, if the placement is left or right, the size is width, otherwise the size is height. pub(super) size: Pixels, + + /// Whether the Dock is open pub(super) open: bool, + /// Whether the Dock is collapsible, default: true pub(super) collapsible: bool, - // Runtime state /// Whether the Dock is resizing is_resizing: bool, } diff --git a/crates/ui/src/dock_area/mod.rs b/crates/dock/src/lib.rs similarity index 98% rename from crates/ui/src/dock_area/mod.rs rename to crates/dock/src/lib.rs index a2fb704..13265cf 100644 --- a/crates/ui/src/dock_area/mod.rs +++ b/crates/dock/src/lib.rs @@ -7,25 +7,17 @@ use gpui::{ ParentElement as _, Pixels, Render, SharedString, Styled, Subscription, WeakEntity, Window, }; -use crate::dock_area::dock::{Dock, DockPlacement}; -use crate::dock_area::panel::{Panel, PanelEvent, PanelStyle, PanelView}; -use crate::dock_area::stack_panel::StackPanel; -use crate::dock_area::tab_panel::TabPanel; +use crate::dock::{Dock, DockPlacement}; +use crate::panel::{Panel, PanelEvent, PanelStyle, PanelView}; +use crate::stack_panel::StackPanel; +use crate::tab_panel::TabPanel; pub mod dock; pub mod panel; pub mod stack_panel; pub mod tab_panel; -actions!( - dock, - [ - /// Zoom the current panel - ToggleZoom, - /// Close the current panel - ClosePanel - ] -); +actions!(dock, [ToggleZoom, ClosePanel]); pub enum DockEvent { /// The layout of the dock has changed, subscribers this to save the layout. diff --git a/crates/ui/src/dock_area/panel.rs b/crates/dock/src/panel.rs similarity index 98% rename from crates/ui/src/dock_area/panel.rs rename to crates/dock/src/panel.rs index 8e64dc6..f1fe9d3 100644 --- a/crates/ui/src/dock_area/panel.rs +++ b/crates/dock/src/panel.rs @@ -2,9 +2,8 @@ use gpui::{ AnyElement, AnyView, App, Element, Entity, EventEmitter, FocusHandle, Focusable, Hsla, Render, SharedString, Window, }; - -use crate::button::Button; -use crate::popup_menu::PopupMenu; +use ui::button::Button; +use ui::popup_menu::PopupMenu; pub enum PanelEvent { ZoomIn, diff --git a/crates/ui/src/dock_area/stack_panel.rs b/crates/dock/src/stack_panel.rs similarity index 98% rename from crates/ui/src/dock_area/stack_panel.rs rename to crates/dock/src/stack_panel.rs index 92fe47a..6a7a18e 100644 --- a/crates/ui/src/dock_area/stack_panel.rs +++ b/crates/dock/src/stack_panel.rs @@ -7,15 +7,15 @@ use gpui::{ Window, }; use smallvec::SmallVec; - -use super::{DockArea, PanelEvent}; -use crate::dock_area::panel::{Panel, PanelView}; -use crate::dock_area::tab_panel::TabPanel; -use crate::resizable::{ +use ui::resizable::{ h_resizable, resizable_panel, v_resizable, ResizablePanel, ResizablePanelEvent, ResizablePanelGroup, }; -use crate::{h_flex, AxisExt as _, Placement}; +use ui::{h_flex, AxisExt as _, Placement}; + +use super::{DockArea, PanelEvent}; +use crate::panel::{Panel, PanelView}; +use crate::tab_panel::TabPanel; pub struct StackPanel { pub(super) parent: Option>, diff --git a/crates/ui/src/dock_area/tab_panel.rs b/crates/dock/src/tab_panel.rs similarity index 97% rename from crates/ui/src/dock_area/tab_panel.rs rename to crates/dock/src/tab_panel.rs index b91e635..0ef0efd 100644 --- a/crates/ui/src/dock_area/tab_panel.rs +++ b/crates/dock/src/tab_panel.rs @@ -8,16 +8,15 @@ use gpui::{ StatefulInteractiveElement, Styled, WeakEntity, Window, }; use theme::ActiveTheme; +use ui::button::{Button, ButtonVariants as _}; +use ui::popup_menu::{PopupMenu, PopupMenuExt}; +use ui::tab::tab_bar::TabBar; +use ui::tab::Tab; +use ui::{h_flex, v_flex, AxisExt, IconName, Placement, Selectable, Sizable, StyledExt}; -use super::panel::PanelView; -use super::stack_panel::StackPanel; -use super::{ClosePanel, DockArea, PanelEvent, PanelStyle, ToggleZoom}; -use crate::button::{Button, ButtonVariants as _}; -use crate::dock_area::panel::Panel; -use crate::popup_menu::{PopupMenu, PopupMenuExt}; -use crate::tab::tab_bar::TabBar; -use crate::tab::Tab; -use crate::{h_flex, v_flex, AxisExt, IconName, Placement, Selectable, Sizable, StyledExt}; +use crate::panel::{Panel, PanelView}; +use crate::stack_panel::StackPanel; +use crate::{ClosePanel, DockArea, PanelEvent, PanelStyle, ToggleZoom}; #[derive(Clone)] struct TabState { @@ -646,13 +645,9 @@ impl TabPanel { .group("") .overflow_hidden() .flex_1() - .p_1() .child( div() .size_full() - .rounded(cx.theme().radius_lg) - .when(cx.theme().shadow, |this| this.shadow_sm()) - .when(cx.theme().mode.is_dark(), |this| this.shadow_lg()) .bg(cx.theme().panel_background) .overflow_hidden() .child( @@ -667,7 +662,6 @@ impl TabPanel { div() .invisible() .absolute() - .p_1() .child( div() .rounded(cx.theme().radius_lg) diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs index a83e5de..5ebaa28 100644 --- a/crates/state/src/lib.rs +++ b/crates/state/src/lib.rs @@ -653,6 +653,8 @@ impl NostrRegistry { // Update the signer self.set_signer(keys, false, cx); + // TODO: set metadata + // Spawn a task to write the credentials cx.background_spawn(async move { if let Err(e) = write_credential.await { diff --git a/crates/title_bar/Cargo.toml b/crates/title_bar/Cargo.toml index ee0cf1f..e6fb439 100644 --- a/crates/title_bar/Cargo.toml +++ b/crates/title_bar/Cargo.toml @@ -9,12 +9,9 @@ common = { path = "../common" } theme = { path = "../theme" } ui = { path = "../ui" } -nostr-sdk.workspace = true gpui.workspace = true -smol.workspace = true smallvec.workspace = true anyhow.workspace = true -log.workspace = true [target.'cfg(target_os = "windows")'.dependencies] windows = { version = "0.61", features = ["Wdk_System_SystemServices"] } diff --git a/crates/title_bar/src/lib.rs b/crates/title_bar/src/lib.rs index 0ff981a..4a718d8 100644 --- a/crates/title_bar/src/lib.rs +++ b/crates/title_bar/src/lib.rs @@ -43,7 +43,7 @@ impl TitleBar { #[cfg(not(target_os = "windows"))] pub fn height(window: &mut Window) -> Pixels { - (1.75 * window.rem_size()).max(px(34.)) + (1.75 * window.rem_size()).max(px(36.)) } #[cfg(target_os = "windows")] diff --git a/crates/title_bar/src/platforms/linux.rs b/crates/title_bar/src/platforms/linux.rs index d285d71..530ed5a 100644 --- a/crates/title_bar/src/platforms/linux.rs +++ b/crates/title_bar/src/platforms/linux.rs @@ -5,7 +5,7 @@ use std::sync::OnceLock; use gpui::prelude::FluentBuilder; use gpui::{ svg, App, InteractiveElement, IntoElement, MouseButton, ParentElement, RenderOnce, - SharedString, StatefulInteractiveElement, Styled, Window, + StatefulInteractiveElement, Styled, Window, }; use linicon::{lookup_icon, IconType}; use theme::ActiveTheme; @@ -70,15 +70,14 @@ impl RenderOnce for WindowControl { .justify_center() .items_center() .rounded_full() + .size_6() .map(|this| { if is_gnome { - this.size_6() - .bg(cx.theme().tab_inactive_background) + this.bg(cx.theme().tab_inactive_background) .hover(|this| this.bg(cx.theme().tab_hover_background)) .active(|this| this.bg(cx.theme().tab_active_background)) } else { - this.size_5() - .bg(cx.theme().ghost_element_background) + this.bg(cx.theme().ghost_element_background) .hover(|this| this.bg(cx.theme().ghost_element_hover)) .active(|this| this.bg(cx.theme().ghost_element_active)) } @@ -87,9 +86,7 @@ impl RenderOnce for WindowControl { if let Some(Some(path)) = linux_controls().get(&self.kind).cloned() { this.child( svg() - .external_path(SharedString::from( - path.into_os_string().into_string().unwrap(), - )) + .external_path(path.into_os_string().into_string().unwrap()) .map(|this| { if cx.theme().is_dark() { this.text_color(gpui::white()) diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index 5875b15..832639b 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -15,7 +15,6 @@ pub mod avatar; pub mod button; pub mod checkbox; pub mod divider; -pub mod dock_area; pub mod dropdown; pub mod history; pub mod indicator; diff --git a/crates/ui/src/resizable/mod.rs b/crates/ui/src/resizable/mod.rs index 1e52ddc..a9f9ff0 100644 --- a/crates/ui/src/resizable/mod.rs +++ b/crates/ui/src/resizable/mod.rs @@ -3,7 +3,7 @@ mod panel; mod resize_handle; pub use panel::*; -pub(crate) use resize_handle::*; +pub use resize_handle::*; pub fn h_resizable( window: &mut Window, diff --git a/crates/ui/src/resizable/panel.rs b/crates/ui/src/resizable/panel.rs index a150982..8885ce0 100644 --- a/crates/ui/src/resizable/panel.rs +++ b/crates/ui/src/resizable/panel.rs @@ -11,7 +11,7 @@ use gpui::{ use super::resize_handle; use crate::{h_flex, v_flex, AxisExt}; -pub(crate) const PANEL_MIN_SIZE: Pixels = px(100.); +pub const PANEL_MIN_SIZE: Pixels = px(100.); pub enum ResizablePanelEvent { Resized, @@ -53,7 +53,7 @@ impl ResizablePanelGroup { self } - pub(crate) fn set_axis(&mut self, axis: Axis, _window: &mut Window, cx: &mut Context) { + pub fn set_axis(&mut self, axis: Axis, _window: &mut Window, cx: &mut Context) { self.axis = axis; cx.notify(); } @@ -130,7 +130,7 @@ impl ResizablePanelGroup { } /// Replace a child panel with a new panel at the given index. - pub(crate) fn replace_child( + pub fn replace_child( &mut self, panel: ResizablePanel, ix: usize, @@ -158,7 +158,7 @@ impl ResizablePanelGroup { cx.notify() } - pub(crate) fn remove_all_children(&mut self, _window: &mut Window, cx: &mut Context) { + pub fn remove_all_children(&mut self, _window: &mut Window, cx: &mut Context) { self.sizes.clear(); self.panels.clear(); cx.notify() @@ -349,7 +349,7 @@ impl ResizablePanel { self } - pub(crate) fn content_visible(mut self, content_visible: F) -> Self + pub fn content_visible(mut self, content_visible: F) -> Self where F: Fn(&App) -> bool + 'static, { diff --git a/crates/ui/src/resizable/resize_handle.rs b/crates/ui/src/resizable/resize_handle.rs index 0a8e131..916ddd3 100644 --- a/crates/ui/src/resizable/resize_handle.rs +++ b/crates/ui/src/resizable/resize_handle.rs @@ -7,11 +7,11 @@ use theme::ActiveTheme; use crate::AxisExt as _; -pub(crate) const HANDLE_PADDING: Pixels = px(8.); -pub(crate) const HANDLE_SIZE: Pixels = px(2.); +pub const HANDLE_PADDING: Pixels = px(8.); +pub const HANDLE_SIZE: Pixels = px(2.); #[derive(IntoElement)] -pub(crate) struct ResizeHandle { +pub struct ResizeHandle { base: Stateful

, axis: Axis, } @@ -26,7 +26,7 @@ impl ResizeHandle { } /// Create a resize handle for a resizable panel. -pub(crate) fn resize_handle(id: impl Into, axis: Axis) -> ResizeHandle { +pub fn resize_handle(id: impl Into, axis: Axis) -> ResizeHandle { ResizeHandle::new(id, axis) } -- 2.49.1 From 6617c8eea387583fd9c81f01f8521acb013b8b67 Mon Sep 17 00:00:00 2001 From: reya Date: Fri, 23 Jan 2026 15:59:19 +0700 Subject: [PATCH 11/30] wip --- Cargo.lock | 16 +- assets/icons/panel-left-open.svg | 3 + assets/icons/panel-left.svg | 3 + crates/coop/Cargo.toml | 1 - crates/coop/src/sidebar/mod.rs | 101 ++-- crates/coop/src/workspace.rs | 64 +- crates/dock/Cargo.toml | 3 + crates/dock/src/dock.rs | 70 +-- crates/dock/src/lib.rs | 35 +- .../src/platforms/linux.rs | 14 +- .../{title_bar => dock}/src/platforms/mod.rs | 1 - .../src/platforms/windows.rs | 0 crates/dock/src/resizable/mod.rs | 294 +++++++++ crates/dock/src/resizable/panel.rs | 405 +++++++++++++ crates/dock/src/resizable/resize_handle.rs | 227 +++++++ crates/dock/src/stack_panel.rs | 125 ++-- crates/{ui => dock}/src/tab/mod.rs | 52 +- crates/dock/src/tab/tab_bar.rs | 143 +++++ crates/dock/src/tab_panel.rs | 256 ++++++-- crates/theme/src/colors.rs | 16 +- crates/theme/src/lib.rs | 11 + .../{title_bar => theme}/src/platform_kind.rs | 0 crates/title_bar/Cargo.toml | 20 - crates/title_bar/src/lib.rs | 183 ------ crates/title_bar/src/platforms/mac.rs | 6 - crates/ui/src/element_ext.rs | 27 + crates/ui/src/icon.rs | 12 + crates/ui/src/lib.rs | 4 +- crates/ui/src/resizable/mod.rs | 24 - crates/ui/src/resizable/panel.rs | 561 ------------------ crates/ui/src/resizable/resize_handle.rs | 74 --- crates/ui/src/root.rs | 1 + crates/ui/src/tab/tab_bar.rs | 85 --- 33 files changed, 1568 insertions(+), 1269 deletions(-) create mode 100644 assets/icons/panel-left-open.svg create mode 100644 assets/icons/panel-left.svg rename crates/{title_bar => dock}/src/platforms/linux.rs (91%) rename crates/{title_bar => dock}/src/platforms/mod.rs (82%) rename crates/{title_bar => dock}/src/platforms/windows.rs (100%) create mode 100644 crates/dock/src/resizable/mod.rs create mode 100644 crates/dock/src/resizable/panel.rs create mode 100644 crates/dock/src/resizable/resize_handle.rs rename crates/{ui => dock}/src/tab/mod.rs (75%) create mode 100644 crates/dock/src/tab/tab_bar.rs rename crates/{title_bar => theme}/src/platform_kind.rs (100%) delete mode 100644 crates/title_bar/Cargo.toml delete mode 100644 crates/title_bar/src/lib.rs delete mode 100644 crates/title_bar/src/platforms/mac.rs create mode 100644 crates/ui/src/element_ext.rs delete mode 100644 crates/ui/src/resizable/mod.rs delete mode 100644 crates/ui/src/resizable/panel.rs delete mode 100644 crates/ui/src/resizable/resize_handle.rs delete mode 100644 crates/ui/src/tab/tab_bar.rs diff --git a/Cargo.lock b/Cargo.lock index 02074c8..591d2a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1317,7 +1317,6 @@ dependencies = [ "smol", "state", "theme", - "title_bar", "tracing-subscriber", "ui", ] @@ -1749,6 +1748,7 @@ dependencies = [ "anyhow", "common", "gpui", + "linicon", "log", "smallvec", "theme", @@ -6591,20 +6591,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" -[[package]] -name = "title_bar" -version = "0.3.0" -dependencies = [ - "anyhow", - "common", - "gpui", - "linicon", - "smallvec", - "theme", - "ui", - "windows 0.61.3", -] - [[package]] name = "tokio" version = "1.49.0" diff --git a/assets/icons/panel-left-open.svg b/assets/icons/panel-left-open.svg new file mode 100644 index 0000000..a0f79d3 --- /dev/null +++ b/assets/icons/panel-left-open.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/panel-left.svg b/assets/icons/panel-left.svg new file mode 100644 index 0000000..c18c700 --- /dev/null +++ b/assets/icons/panel-left.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/coop/Cargo.toml b/crates/coop/Cargo.toml index 2147bc7..59a81e3 100644 --- a/crates/coop/Cargo.toml +++ b/crates/coop/Cargo.toml @@ -30,7 +30,6 @@ icons = [ assets = { path = "../assets" } ui = { path = "../ui" } dock = { path = "../dock" } -title_bar = { path = "../title_bar" } theme = { path = "../theme" } common = { path = "../common" } state = { path = "../state" } diff --git a/crates/coop/src/sidebar/mod.rs b/crates/coop/src/sidebar/mod.rs index 2cf5ef8..f6de5d8 100644 --- a/crates/coop/src/sidebar/mod.rs +++ b/crates/coop/src/sidebar/mod.rs @@ -7,22 +7,25 @@ use common::{DebouncedDelay, RenderedTimestamp, TextUtils, BOOTSTRAP_RELAYS, SEA use dock::panel::{Panel, PanelEvent}; use gpui::prelude::FluentBuilder; use gpui::{ - deferred, div, relative, uniform_list, App, AppContext, Context, Entity, EventEmitter, + deferred, div, relative, rems, uniform_list, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Render, RetainAllImageCache, SharedString, Styled, Subscription, Task, Window, }; use gpui_tokio::Tokio; use list_item::RoomListItem; use nostr_sdk::prelude::*; +use person::PersonRegistry; use smallvec::{smallvec, SmallVec}; use state::{NostrRegistry, GIFTWRAP_SUBSCRIPTION}; -use theme::ActiveTheme; +use theme::{ActiveTheme, TITLEBAR_HEIGHT}; +use ui::avatar::Avatar; use ui::button::{Button, ButtonVariants}; use ui::input::{InputEvent, InputState, TextInput}; use ui::popup_menu::PopupMenuExt; use ui::{h_flex, v_flex, Icon, IconName, Selectable, Sizable, StyledExt, WindowExtension}; use crate::actions::{RelayStatus, Reload}; +use crate::views::compose::compose_button; mod list_item; @@ -589,6 +592,13 @@ impl Focusable for Sidebar { impl Render for Sidebar { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + const EMPTY_HELP: &str = "Start a conversation with someone to get started."; + const REQUEST_HELP: &str = + "New message requests from people you don't know will appear here."; + + let nostr = NostrRegistry::global(cx); + let identity = nostr.read(cx).identity(); + let chat = ChatRegistry::global(cx); let loading = chat.read(cx).loading(); @@ -619,36 +629,53 @@ impl Render for Sidebar { .size_full() .relative() .gap_3() + // Titlebar + .child( + h_flex().h(TITLEBAR_HEIGHT).w_full().items_center().child( + h_flex() + .h_6() + .w_full() + .gap_2() + .justify_between() + .when_some(identity.read(cx).public_key, |this, public_key| { + let persons = PersonRegistry::global(cx); + let profile = persons.read(cx).get(&public_key, cx); + + this.child( + Button::new("user") + .small() + .reverse() + .transparent() + .icon(IconName::CaretDown) + .child(Avatar::new(profile.avatar()).size(rems(1.5))), + ) + }) + .child(div().pr_2p5().child(compose_button())), + ), + ) // Search Input .child( - div() - .relative() - .mt_3() - .px_2p5() - .w_full() - .h_7() - .flex_none() - .flex() - .child( - TextInput::new(&self.find_input) - .small() - .cleanable() - .appearance(true) - .text_xs() - .map(|this| { - if !self.find_input.read(cx).loading { - this.suffix( - Button::new("find") - .icon(IconName::Search) - .tooltip("Press Enter to search") - .transparent() - .small(), - ) - } else { - this - } - }), - ), + div().px_2p5().child( + TextInput::new(&self.find_input) + .small() + .cleanable() + .appearance(true) + .text_xs() + .flex_none() + .map(|this| { + if !self.find_input.read(cx).loading { + this.suffix( + Button::new("find") + .icon(IconName::Search) + .tooltip("Press Enter to search") + .transparent() + .small(), + ) + } else { + this + } + }), + ), ) // Chat Rooms .child( @@ -711,14 +738,8 @@ impl Render for Sidebar { .ghost() .rounded() .popup_menu(move |this, _window, _cx| { - this.menu( - "Reload", - Box::new(Reload), - ) - .menu( - "Relay Status", - Box::new(RelayStatus), - ) + this.menu("Reload", Box::new(Reload)) + .menu("Relay Status", Box::new(RelayStatus)) }), ), ), @@ -746,7 +767,7 @@ impl Render for Sidebar { .text_xs() .text_color(cx.theme().text_muted) .line_height(relative(1.25)) - .child(SharedString::from("Start a conversation with someone to get started.")), + .child(SharedString::from(EMPTY_HELP)), ), )) } else { @@ -770,7 +791,7 @@ impl Render for Sidebar { .text_xs() .text_color(cx.theme().text_muted) .line_height(relative(1.25)) - .child(SharedString::from("New message requests from people you don't know will appear here.")), + .child(SharedString::from(REQUEST_HELP)), ), )) } diff --git a/crates/coop/src/workspace.rs b/crates/coop/src/workspace.rs index b68d7a4..30860f4 100644 --- a/crates/coop/src/workspace.rs +++ b/crates/coop/src/workspace.rs @@ -1,35 +1,26 @@ use std::sync::Arc; -use auto_update::{AutoUpdateStatus, AutoUpdater}; use chat::{ChatEvent, ChatRegistry}; use chat_ui::{CopyPublicKey, OpenPublicKey}; use common::DEFAULT_SIDEBAR_WIDTH; use dock::dock::DockPlacement; use dock::{ClosePanel, DockArea, DockItem}; -use gpui::prelude::FluentBuilder; use gpui::{ - deferred, div, px, relative, rems, App, AppContext, Axis, ClipboardItem, Context, Entity, - InteractiveElement, IntoElement, ParentElement, Render, SharedString, - StatefulInteractiveElement, Styled, Subscription, Window, + div, px, relative, App, AppContext, Axis, ClipboardItem, Context, Entity, InteractiveElement, + IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Window, }; use nostr_connect::prelude::*; use person::PersonRegistry; -use relay_auth::RelayAuth; use smallvec::{smallvec, SmallVec}; -use state::NostrRegistry; use theme::{ActiveTheme, Theme, ThemeMode, ThemeRegistry}; -use title_bar::TitleBar; -use ui::avatar::Avatar; use ui::button::{Button, ButtonVariants}; use ui::modal::ModalButtonProps; -use ui::popup_menu::PopupMenuExt; -use ui::{h_flex, v_flex, IconName, Root, Sizable, WindowExtension}; +use ui::{h_flex, v_flex, Root, Sizable, WindowExtension}; use crate::actions::{ reset, DarkMode, KeyringPopup, Logout, Settings, Themes, ViewProfile, ViewRelays, }; use crate::user::viewer; -use crate::views::compose::compose_button; use crate::views::{preferences, setup_relay, welcome}; use crate::{sidebar, user}; @@ -39,9 +30,6 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity { #[derive(Debug)] pub struct Workspace { - /// App's Title Bar - title_bar: Entity, - /// App's Dock Area dock: Entity, @@ -52,8 +40,8 @@ pub struct Workspace { impl Workspace { fn new(window: &mut Window, cx: &mut Context) -> Self { let chat = ChatRegistry::global(cx); - let title_bar = cx.new(|_| TitleBar::new()); - let dock = cx.new(|cx| DockArea::new(window, cx)); + let dock = + cx.new(|cx| DockArea::new(window, cx).panel_style(dock::panel::PanelStyle::TabBar)); let mut subscriptions = smallvec![]; @@ -115,7 +103,6 @@ impl Workspace { Self { dock, - title_bar, _subscriptions: subscriptions, } } @@ -353,44 +340,7 @@ impl Workspace { Some(ids) } - fn titlebar_left(&mut self, _window: &mut Window, cx: &Context) -> impl IntoElement { - let nostr = NostrRegistry::global(cx); - let identity = nostr.read(cx).identity(); - - h_flex() - .gap_2() - .h_6() - .w_full() - .when_some(identity.read(cx).public_key, |this, public_key| { - let persons = PersonRegistry::global(cx); - let profile = persons.read(cx).get(&public_key, cx); - - this.child( - Button::new("user") - .small() - .reverse() - .transparent() - .icon(IconName::CaretDown) - .child(Avatar::new(profile.avatar()).size(rems(1.5))) - .popup_menu(move |this, _window, _cx| { - this.label(profile.name()) - .menu_with_icon("Profile", IconName::Emoji, Box::new(ViewProfile)) - .menu_with_icon( - "Messaging Relays", - IconName::Relay, - Box::new(ViewRelays), - ) - .separator() - .menu_with_icon("Dark Mode", IconName::Sun, Box::new(DarkMode)) - .menu_with_icon("Themes", IconName::Moon, Box::new(Themes)) - .menu_with_icon("Settings", IconName::Settings, Box::new(Settings)) - .menu_with_icon("Sign Out", IconName::Door, Box::new(Logout)) - }), - ) - }) - .child(compose_button()) - } - + /* fn titlebar_right(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let chat = ChatRegistry::global(cx); let status = chat.read(cx).loading(); @@ -471,7 +421,7 @@ impl Workspace { )), )) }) - } + }*/ } impl Render for Workspace { diff --git a/crates/dock/Cargo.toml b/crates/dock/Cargo.toml index 36830b8..0697dfb 100644 --- a/crates/dock/Cargo.toml +++ b/crates/dock/Cargo.toml @@ -13,3 +13,6 @@ gpui.workspace = true smallvec.workspace = true anyhow.workspace = true log.workspace = true + +[target.'cfg(target_os = "linux")'.dependencies] +linicon = "2.3.0" diff --git a/crates/dock/src/dock.rs b/crates/dock/src/dock.rs index 603510b..a535304 100644 --- a/crates/dock/src/dock.rs +++ b/crates/dock/src/dock.rs @@ -1,17 +1,17 @@ +use std::ops::Deref; use std::sync::Arc; use gpui::prelude::FluentBuilder as _; use gpui::{ - div, px, App, AppContext, Axis, Context, Element, Entity, InteractiveElement as _, IntoElement, - MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Point, Render, - StatefulInteractiveElement, Style, Styled as _, WeakEntity, Window, + div, px, App, AppContext, Axis, Context, Element, Entity, IntoElement, MouseMoveEvent, + MouseUpEvent, ParentElement as _, Pixels, Point, Render, Style, Styled as _, WeakEntity, + Window, }; -use theme::ActiveTheme; -use ui::resizable::{HANDLE_PADDING, HANDLE_SIZE, PANEL_MIN_SIZE}; -use ui::{AxisExt as _, StyledExt}; +use ui::StyledExt; use super::{DockArea, DockItem}; use crate::panel::PanelView; +use crate::resizable::{resize_handle, PANEL_MIN_SIZE}; use crate::tab_panel::TabPanel; #[derive(Clone, Render)] @@ -67,7 +67,7 @@ pub struct Dock { pub(super) collapsible: bool, /// Whether the Dock is resizing - is_resizing: bool, + resizing: bool, } impl Dock { @@ -98,7 +98,7 @@ impl Dock { open: true, collapsible: true, size: px(200.0), - is_resizing: false, + resizing: false, } } @@ -231,54 +231,16 @@ impl Dock { cx: &mut Context, ) -> impl IntoElement { let axis = self.placement.axis(); - let neg_offset = -HANDLE_PADDING; let view = cx.entity().clone(); - div() - .id("resize-handle") - .occlude() - .absolute() - .flex_shrink_0() - .when(self.placement.is_left(), |this| { - // FIXME: Improve this to let the scroll bar have px(HANDLE_PADDING) - this.cursor_col_resize() - .top_0() - .right(px(1.)) - .h_full() - .w(HANDLE_SIZE) - .pt_12() - .pb_4() - }) - .when(self.placement.is_right(), |this| { - this.cursor_col_resize() - .top_0() - .left(px(-0.5)) - .h_full() - .w(HANDLE_SIZE) - .pt_12() - .pb_4() - }) - .when(self.placement.is_bottom(), |this| { - this.cursor_row_resize() - .top(neg_offset) - .left_0() - .w_full() - .h(HANDLE_SIZE) - .py(HANDLE_PADDING) - }) - .child( - div() - .rounded_full() - .hover(|this| this.bg(cx.theme().border_variant)) - .when(axis.is_horizontal(), |this| this.h_full().w(HANDLE_SIZE)) - .when(axis.is_vertical(), |this| this.w_full().h(HANDLE_SIZE)), - ) + resize_handle("resize-handle", axis) + .placement(self.placement) .on_drag(ResizePanel {}, move |info, _, _, cx| { cx.stop_propagation(); - view.update(cx, |view, _| { - view.is_resizing = true; + view.update(cx, |view, _cx| { + view.resizing = true; }); - cx.new(|_| info.clone()) + cx.new(|_| info.deref().clone()) }) } @@ -288,7 +250,7 @@ impl Dock { _window: &mut Window, cx: &mut Context, ) { - if !self.is_resizing { + if !self.resizing { return; } @@ -349,7 +311,7 @@ impl Dock { } fn done_resizing(&mut self, _window: &mut Window, _cx: &mut Context) { - self.is_resizing = false; + self.resizing = false; } } @@ -440,7 +402,7 @@ impl Element for DockElement { ) { window.on_mouse_event({ let view = self.view.clone(); - let is_resizing = view.read(cx).is_resizing; + let is_resizing = view.read(cx).resizing; move |e: &MouseMoveEvent, phase, window, cx| { if !is_resizing { return; diff --git a/crates/dock/src/lib.rs b/crates/dock/src/lib.rs index 13265cf..5c78663 100644 --- a/crates/dock/src/lib.rs +++ b/crates/dock/src/lib.rs @@ -2,10 +2,11 @@ use std::sync::Arc; use gpui::prelude::FluentBuilder; use gpui::{ - actions, canvas, div, px, AnyElement, AnyView, App, AppContext, Axis, Bounds, Context, Edges, - Entity, EntityId, EventEmitter, Focusable, InteractiveElement as _, IntoElement, - ParentElement as _, Pixels, Render, SharedString, Styled, Subscription, WeakEntity, Window, + actions, div, px, AnyElement, AnyView, App, AppContext, Axis, Bounds, Context, Edges, Entity, + EntityId, EventEmitter, Focusable, InteractiveElement as _, IntoElement, ParentElement as _, + Pixels, Render, SharedString, Styled, Subscription, WeakEntity, Window, }; +use ui::ElementExt; use crate::dock::{Dock, DockPlacement}; use crate::panel::{Panel, PanelEvent, PanelStyle, PanelView}; @@ -14,7 +15,10 @@ use crate::tab_panel::TabPanel; pub mod dock; pub mod panel; +mod platforms; +pub mod resizable; pub mod stack_panel; +pub mod tab; pub mod tab_panel; actions!(dock, [ToggleZoom, ClosePanel]); @@ -30,20 +34,31 @@ pub enum DockEvent { /// The main area of the dock. pub struct DockArea { pub(crate) bounds: Bounds, + /// The center view of the dockarea. pub items: DockItem, - /// The entity_id of the [`TabPanel`](TabPanel) where each toggle button should be displayed, - toggle_button_panels: Edges>, + /// The left dock of the dock_area. left_dock: Option>, + /// The bottom dock of the dock_area. bottom_dock: Option>, + /// The right dock of the dock_area. right_dock: Option>, + + /// The entity_id of the [`TabPanel`](TabPanel) where each toggle button should be displayed, + toggle_button_panels: Edges>, + + /// Whether to show the toggle button. + toggle_button_visible: bool, + /// The top zoom view of the dock_area, if any. zoom_view: Option, + /// Lock panels layout, but allow to resize. is_locked: bool, + /// The panel style, default is [`PanelStyle::Default`](PanelStyle::Default). pub(crate) panel_style: PanelStyle, subscriptions: Vec, @@ -322,6 +337,7 @@ impl DockArea { items: dock_item, zoom_view: None, toggle_button_panels: Edges::default(), + toggle_button_visible: true, left_dock: None, right_dock: None, bottom_dock: None, @@ -738,14 +754,7 @@ impl Render for DockArea { .relative() .size_full() .overflow_hidden() - .child( - canvas( - move |bounds, _, cx| view.update(cx, |r, _| r.bounds = bounds), - |_, _, _, _| {}, - ) - .absolute() - .size_full(), - ) + .on_prepaint(move |bounds, _, cx| view.update(cx, |r, _| r.bounds = bounds)) .map(|this| { if let Some(zoom_view) = self.zoom_view.clone() { this.child(zoom_view) diff --git a/crates/title_bar/src/platforms/linux.rs b/crates/dock/src/platforms/linux.rs similarity index 91% rename from crates/title_bar/src/platforms/linux.rs rename to crates/dock/src/platforms/linux.rs index 530ed5a..2e675de 100644 --- a/crates/title_bar/src/platforms/linux.rs +++ b/crates/dock/src/platforms/linux.rs @@ -55,6 +55,7 @@ impl WindowControl { Self { kind, fallback } } + #[allow(dead_code)] pub fn is_gnome(&self) -> bool { matches!(detect_desktop_environment(), DesktopEnvironment::Gnome) } @@ -62,8 +63,6 @@ impl WindowControl { impl RenderOnce for WindowControl { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let is_gnome = self.is_gnome(); - h_flex() .id(self.kind.as_icon_name()) .group("") @@ -71,17 +70,6 @@ impl RenderOnce for WindowControl { .items_center() .rounded_full() .size_6() - .map(|this| { - if is_gnome { - this.bg(cx.theme().tab_inactive_background) - .hover(|this| this.bg(cx.theme().tab_hover_background)) - .active(|this| this.bg(cx.theme().tab_active_background)) - } else { - this.bg(cx.theme().ghost_element_background) - .hover(|this| this.bg(cx.theme().ghost_element_hover)) - .active(|this| this.bg(cx.theme().ghost_element_active)) - } - }) .map(|this| { if let Some(Some(path)) = linux_controls().get(&self.kind).cloned() { this.child( diff --git a/crates/title_bar/src/platforms/mod.rs b/crates/dock/src/platforms/mod.rs similarity index 82% rename from crates/title_bar/src/platforms/mod.rs rename to crates/dock/src/platforms/mod.rs index e0ff781..f880e5d 100644 --- a/crates/title_bar/src/platforms/mod.rs +++ b/crates/dock/src/platforms/mod.rs @@ -1,4 +1,3 @@ #[cfg(target_os = "linux")] pub mod linux; -pub mod mac; pub mod windows; diff --git a/crates/title_bar/src/platforms/windows.rs b/crates/dock/src/platforms/windows.rs similarity index 100% rename from crates/title_bar/src/platforms/windows.rs rename to crates/dock/src/platforms/windows.rs diff --git a/crates/dock/src/resizable/mod.rs b/crates/dock/src/resizable/mod.rs new file mode 100644 index 0000000..ce66f14 --- /dev/null +++ b/crates/dock/src/resizable/mod.rs @@ -0,0 +1,294 @@ +use std::ops::Range; + +use gpui::{ + px, Along, App, Axis, Bounds, Context, ElementId, EventEmitter, IsZero, Pixels, Window, +}; + +mod panel; +mod resize_handle; +pub use panel::*; +pub(crate) use resize_handle::*; + +pub(crate) const PANEL_MIN_SIZE: Pixels = px(100.); + +/// Create a [`ResizablePanelGroup`] with horizontal resizing +pub fn h_resizable(id: impl Into) -> ResizablePanelGroup { + ResizablePanelGroup::new(id).axis(Axis::Horizontal) +} + +/// Create a [`ResizablePanelGroup`] with vertical resizing +pub fn v_resizable(id: impl Into) -> ResizablePanelGroup { + ResizablePanelGroup::new(id).axis(Axis::Vertical) +} + +/// Create a [`ResizablePanel`]. +pub fn resizable_panel() -> ResizablePanel { + ResizablePanel::new() +} + +/// State for a [`ResizablePanel`] +#[derive(Debug, Clone)] +pub struct ResizableState { + /// The `axis` will sync to actual axis of the ResizablePanelGroup in use. + axis: Axis, + panels: Vec, + sizes: Vec, + pub(crate) resizing_panel_ix: Option, + bounds: Bounds, +} + +impl Default for ResizableState { + fn default() -> Self { + Self { + axis: Axis::Horizontal, + panels: vec![], + sizes: vec![], + resizing_panel_ix: None, + bounds: Bounds::default(), + } + } +} + +impl ResizableState { + /// Get the size of the panels. + pub fn sizes(&self) -> &Vec { + &self.sizes + } + + pub(crate) fn insert_panel( + &mut self, + size: Option, + ix: Option, + cx: &mut Context, + ) { + let panel_state = ResizablePanelState { + size, + ..Default::default() + }; + + let size = size.unwrap_or(PANEL_MIN_SIZE); + + // We make sure that the size always sums up to the container size + // by reducing the size of all other panels first. + let container_size = self.container_size().max(px(1.)); + let total_leftover_size = (container_size - size).max(px(1.)); + + for (i, panel) in self.panels.iter_mut().enumerate() { + let ratio = self.sizes[i] / container_size; + self.sizes[i] = total_leftover_size * ratio; + panel.size = Some(self.sizes[i]); + } + + if let Some(ix) = ix { + self.panels.insert(ix, panel_state); + self.sizes.insert(ix, size); + } else { + self.panels.push(panel_state); + self.sizes.push(size); + }; + + cx.notify(); + } + + pub(crate) fn sync_panels_count( + &mut self, + axis: Axis, + panels_count: usize, + cx: &mut Context, + ) { + let mut changed = self.axis != axis; + self.axis = axis; + + if panels_count > self.panels.len() { + let diff = panels_count - self.panels.len(); + self.panels + .extend(vec![ResizablePanelState::default(); diff]); + self.sizes.extend(vec![PANEL_MIN_SIZE; diff]); + changed = true; + } + + if panels_count < self.panels.len() { + self.panels.truncate(panels_count); + self.sizes.truncate(panels_count); + changed = true; + } + + if changed { + // We need to make sure the total size is in line with the container size. + self.adjust_to_container_size(cx); + } + } + + pub(crate) fn update_panel_size( + &mut self, + panel_ix: usize, + bounds: Bounds, + size_range: Range, + cx: &mut Context, + ) { + let size = bounds.size.along(self.axis); + // This check is only necessary to stop the very first panel from resizing on its own + // it needs to be passed when the panel is freshly created so we get the initial size, + // but its also fine when it sometimes passes later. + if self.sizes[panel_ix].to_f64() == PANEL_MIN_SIZE.to_f64() { + self.sizes[panel_ix] = size; + self.panels[panel_ix].size = Some(size); + } + self.panels[panel_ix].bounds = bounds; + self.panels[panel_ix].size_range = size_range; + cx.notify(); + } + + pub(crate) fn remove_panel(&mut self, panel_ix: usize, cx: &mut Context) { + self.panels.remove(panel_ix); + self.sizes.remove(panel_ix); + if let Some(resizing_panel_ix) = self.resizing_panel_ix { + if resizing_panel_ix > panel_ix { + self.resizing_panel_ix = Some(resizing_panel_ix - 1); + } + } + self.adjust_to_container_size(cx); + } + + pub(crate) fn replace_panel( + &mut self, + panel_ix: usize, + panel: ResizablePanelState, + cx: &mut Context, + ) { + let old_size = self.sizes[panel_ix]; + + self.panels[panel_ix] = panel; + self.sizes[panel_ix] = old_size; + self.adjust_to_container_size(cx); + } + + pub(crate) fn clear(&mut self) { + self.panels.clear(); + self.sizes.clear(); + } + + #[inline] + pub(crate) fn container_size(&self) -> Pixels { + self.bounds.size.along(self.axis) + } + + pub(crate) fn done_resizing(&mut self, cx: &mut Context) { + self.resizing_panel_ix = None; + cx.emit(ResizablePanelEvent::Resized); + } + + fn panel_size_range(&self, ix: usize) -> Range { + let Some(panel) = self.panels.get(ix) else { + return PANEL_MIN_SIZE..Pixels::MAX; + }; + + panel.size_range.clone() + } + + fn sync_real_panel_sizes(&mut self, _: &App) { + for (i, panel) in self.panels.iter().enumerate() { + self.sizes[i] = panel.bounds.size.along(self.axis); + } + } + + /// The `ix`` is the index of the panel to resize, + /// and the `size` is the new size for the panel. + fn resize_panel(&mut self, ix: usize, size: Pixels, _: &mut Window, cx: &mut Context) { + let old_sizes = self.sizes.clone(); + + let mut ix = ix; + // Only resize the left panels. + if ix >= old_sizes.len() - 1 { + return; + } + let container_size = self.container_size(); + self.sync_real_panel_sizes(cx); + + let move_changed = size - old_sizes[ix]; + if move_changed == px(0.) { + return; + } + + let size_range = self.panel_size_range(ix); + let new_size = size.clamp(size_range.start, size_range.end); + let is_expand = move_changed > px(0.); + + let main_ix = ix; + let mut new_sizes = old_sizes.clone(); + + if is_expand { + let mut changed = new_size - old_sizes[ix]; + new_sizes[ix] = new_size; + + while changed > px(0.) && ix < old_sizes.len() - 1 { + ix += 1; + let size_range = self.panel_size_range(ix); + let available_size = (new_sizes[ix] - size_range.start).max(px(0.)); + let to_reduce = changed.min(available_size); + new_sizes[ix] -= to_reduce; + changed -= to_reduce; + } + } else { + let mut changed = new_size - size; + new_sizes[ix] = new_size; + + while changed > px(0.) && ix > 0 { + ix -= 1; + let size_range = self.panel_size_range(ix); + let available_size = (new_sizes[ix] - size_range.start).max(px(0.)); + let to_reduce = changed.min(available_size); + changed -= to_reduce; + new_sizes[ix] -= to_reduce; + } + + new_sizes[main_ix + 1] += old_sizes[main_ix] - size - changed; + } + + let total_size: Pixels = new_sizes.iter().map(|s| s.to_f64()).sum::().into(); + + // If total size exceeds container size, adjust the main panel + if total_size > container_size { + let overflow = total_size - container_size; + new_sizes[main_ix] = (new_sizes[main_ix] - overflow).max(size_range.start); + } + + for (i, _) in old_sizes.iter().enumerate() { + let size = new_sizes[i]; + self.panels[i].size = Some(size); + } + self.sizes = new_sizes; + cx.notify(); + } + + /// Adjust panel sizes according to the container size. + /// + /// When the container size changes, the panels should take up the same percentage as they did before. + fn adjust_to_container_size(&mut self, cx: &mut Context) { + if self.container_size().is_zero() { + return; + } + + let container_size = self.container_size(); + let total_size = px(self.sizes.iter().map(f32::from).sum::()); + + for i in 0..self.panels.len() { + let size = self.sizes[i]; + let ratio = size / total_size; + let new_size = container_size * ratio; + + self.sizes[i] = new_size; + self.panels[i].size = Some(new_size); + } + cx.notify(); + } +} + +impl EventEmitter for ResizableState {} + +#[derive(Debug, Clone, Default)] +pub(crate) struct ResizablePanelState { + pub size: Option, + pub size_range: Range, + bounds: Bounds, +} diff --git a/crates/dock/src/resizable/panel.rs b/crates/dock/src/resizable/panel.rs new file mode 100644 index 0000000..f72269d --- /dev/null +++ b/crates/dock/src/resizable/panel.rs @@ -0,0 +1,405 @@ +use std::ops::{Deref, Range}; +use std::rc::Rc; + +use gpui::prelude::FluentBuilder; +use gpui::{ + div, Along, AnyElement, App, AppContext, Axis, Bounds, Context, Element, ElementId, Empty, + Entity, EventEmitter, InteractiveElement as _, IntoElement, IsZero as _, MouseMoveEvent, + MouseUpEvent, ParentElement, Pixels, Render, RenderOnce, Style, Styled, Window, +}; +use ui::{h_flex, v_flex, AxisExt, ElementExt}; + +use super::{resizable_panel, resize_handle, ResizableState}; +use crate::resizable::PANEL_MIN_SIZE; + +pub enum ResizablePanelEvent { + Resized, +} + +#[derive(Clone)] +pub(crate) struct DragPanel; +impl Render for DragPanel { + fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement { + Empty + } +} + +/// A group of resizable panels. +#[allow(clippy::type_complexity)] +#[derive(IntoElement)] +pub struct ResizablePanelGroup { + id: ElementId, + state: Option>, + axis: Axis, + size: Option, + children: Vec, + on_resize: Rc, &mut Window, &mut App)>, +} + +impl ResizablePanelGroup { + /// Create a new resizable panel group. + pub fn new(id: impl Into) -> Self { + Self { + id: id.into(), + axis: Axis::Horizontal, + children: vec![], + state: None, + size: None, + on_resize: Rc::new(|_, _, _| {}), + } + } + + /// Bind yourself to a resizable state entity. + /// + /// If not provided, it will handle its own state internally. + pub fn with_state(mut self, state: &Entity) -> Self { + self.state = Some(state.clone()); + self + } + + /// Set the axis of the resizable panel group, default is horizontal. + pub fn axis(mut self, axis: Axis) -> Self { + self.axis = axis; + self + } + + /// Add a panel to the group. + /// + /// - The `axis` will be set to the same axis as the group. + /// - The `initial_size` will be set to the average size of all panels if not provided. + /// - The `group` will be set to the group entity. + pub fn child(mut self, panel: impl Into) -> Self { + self.children.push(panel.into()); + self + } + + /// Add multiple panels to the group. + pub fn children(mut self, panels: impl IntoIterator) -> Self + where + I: Into, + { + self.children = panels.into_iter().map(|panel| panel.into()).collect(); + self + } + + /// Set size of the resizable panel group + /// + /// - When the axis is horizontal, the size is the height of the group. + /// - When the axis is vertical, the size is the width of the group. + pub fn size(mut self, size: Pixels) -> Self { + self.size = Some(size); + self + } + + /// Set the callback to be called when the panels are resized. + /// + /// ## Callback arguments + /// + /// - Entity: The state of the ResizablePanelGroup. + pub fn on_resize( + mut self, + on_resize: impl Fn(&Entity, &mut Window, &mut App) + 'static, + ) -> Self { + self.on_resize = Rc::new(on_resize); + self + } +} + +impl From for ResizablePanel +where + T: Into, +{ + fn from(value: T) -> Self { + resizable_panel().child(value.into()) + } +} + +impl From for ResizablePanel { + fn from(value: ResizablePanelGroup) -> Self { + resizable_panel().child(value) + } +} + +impl EventEmitter for ResizablePanelGroup {} + +impl RenderOnce for ResizablePanelGroup { + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { + let state = self.state.unwrap_or( + window.use_keyed_state(self.id.clone(), cx, |_, _| ResizableState::default()), + ); + let container = if self.axis.is_horizontal() { + h_flex() + } else { + v_flex() + }; + + // Sync panels to the state + let panels_count = self.children.len(); + state.update(cx, |state, cx| { + state.sync_panels_count(self.axis, panels_count, cx); + }); + + container + .id(self.id) + .size_full() + .children( + self.children + .into_iter() + .enumerate() + .map(|(ix, mut panel)| { + panel.panel_ix = ix; + panel.axis = self.axis; + panel.state = Some(state.clone()); + panel + }), + ) + .on_prepaint({ + let state = state.clone(); + move |bounds, _, cx| { + state.update(cx, |state, cx| { + let size_changed = + state.bounds.size.along(self.axis) != bounds.size.along(self.axis); + + state.bounds = bounds; + + if size_changed { + state.adjust_to_container_size(cx); + } + }) + } + }) + .child(ResizePanelGroupElement { + state: state.clone(), + axis: self.axis, + on_resize: self.on_resize.clone(), + }) + } +} + +/// A resizable panel inside a [`ResizablePanelGroup`]. +#[derive(IntoElement)] +pub struct ResizablePanel { + axis: Axis, + panel_ix: usize, + state: Option>, + /// Initial size is the size that the panel has when it is created. + initial_size: Option, + /// size range limit of this panel. + size_range: Range, + children: Vec, + visible: bool, +} + +impl ResizablePanel { + /// Create a new resizable panel. + pub(super) fn new() -> Self { + Self { + panel_ix: 0, + initial_size: None, + state: None, + size_range: (PANEL_MIN_SIZE..Pixels::MAX), + axis: Axis::Horizontal, + children: vec![], + visible: true, + } + } + + /// Set the visibility of the panel, default is true. + pub fn visible(mut self, visible: bool) -> Self { + self.visible = visible; + self + } + + /// Set the initial size of the panel. + pub fn size(mut self, size: impl Into) -> Self { + self.initial_size = Some(size.into()); + self + } + + /// Set the size range to limit panel resize. + /// + /// Default is [`PANEL_MIN_SIZE`] to [`Pixels::MAX`]. + pub fn size_range(mut self, range: impl Into>) -> Self { + self.size_range = range.into(); + self + } +} + +impl ParentElement for ResizablePanel { + fn extend(&mut self, elements: impl IntoIterator) { + self.children.extend(elements); + } +} + +impl RenderOnce for ResizablePanel { + fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { + if !self.visible { + return div().id(("resizable-panel", self.panel_ix)); + } + + let state = self + .state + .expect("BUG: The `state` in ResizablePanel should be present."); + let panel_state = state + .read(cx) + .panels + .get(self.panel_ix) + .expect("BUG: The `index` of ResizablePanel should be one of in `state`."); + let size_range = self.size_range.clone(); + + div() + .id(("resizable-panel", self.panel_ix)) + .flex() + .flex_grow() + .size_full() + .relative() + .when(self.axis.is_vertical(), |this| { + this.min_h(size_range.start).max_h(size_range.end) + }) + .when(self.axis.is_horizontal(), |this| { + this.min_w(size_range.start).max_w(size_range.end) + }) + // 1. initial_size is None, to use auto size. + // 2. initial_size is Some and size is none, to use the initial size of the panel for first time render. + // 3. initial_size is Some and size is Some, use `size`. + .when(self.initial_size.is_none(), |this| this.flex_shrink()) + .when_some(self.initial_size, |this, initial_size| { + // The `self.size` is None, that mean the initial size for the panel, + // so we need set `flex_shrink_0` To let it keep the initial size. + this.when( + panel_state.size.is_none() && !initial_size.is_zero(), + |this| this.flex_none(), + ) + .flex_basis(initial_size) + }) + .map(|this| match panel_state.size { + Some(size) => this.flex_basis(size.min(size_range.end).max(size_range.start)), + None => this, + }) + .on_prepaint({ + let state = state.clone(); + move |bounds, _, cx| { + state.update(cx, |state, cx| { + state.update_panel_size(self.panel_ix, bounds, self.size_range, cx) + }) + } + }) + .children(self.children) + .when(self.panel_ix > 0, |this| { + let ix = self.panel_ix - 1; + this.child(resize_handle(("resizable-handle", ix), self.axis).on_drag( + DragPanel, + move |drag_panel, _, _, cx| { + cx.stop_propagation(); + // Set current resizing panel ix + state.update(cx, |state, _| { + state.resizing_panel_ix = Some(ix); + }); + cx.new(|_| drag_panel.deref().clone()) + }, + )) + }) + } +} + +#[allow(clippy::type_complexity)] +struct ResizePanelGroupElement { + state: Entity, + on_resize: Rc, &mut Window, &mut App)>, + axis: Axis, +} + +impl IntoElement for ResizePanelGroupElement { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl Element for ResizePanelGroupElement { + type PrepaintState = (); + type RequestLayoutState = (); + + fn id(&self) -> Option { + None + } + + fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + _: Option<&gpui::GlobalElementId>, + _: Option<&gpui::InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (gpui::LayoutId, Self::RequestLayoutState) { + (window.request_layout(Style::default(), None, cx), ()) + } + + fn prepaint( + &mut self, + _: Option<&gpui::GlobalElementId>, + _: Option<&gpui::InspectorElementId>, + _: Bounds, + _: &mut Self::RequestLayoutState, + _window: &mut Window, + _cx: &mut App, + ) -> Self::PrepaintState { + } + + fn paint( + &mut self, + _: Option<&gpui::GlobalElementId>, + _: Option<&gpui::InspectorElementId>, + _: Bounds, + _: &mut Self::RequestLayoutState, + _: &mut Self::PrepaintState, + window: &mut Window, + cx: &mut App, + ) { + window.on_mouse_event({ + let state = self.state.clone(); + let axis = self.axis; + let current_ix = state.read(cx).resizing_panel_ix; + move |e: &MouseMoveEvent, phase, window, cx| { + if !phase.bubble() { + return; + } + let Some(ix) = current_ix else { return }; + + state.update(cx, |state, cx| { + let panel = state.panels.get(ix).expect("BUG: invalid panel index"); + + match axis { + Axis::Horizontal => { + state.resize_panel(ix, e.position.x - panel.bounds.left(), window, cx) + } + Axis::Vertical => { + state.resize_panel(ix, e.position.y - panel.bounds.top(), window, cx); + } + } + cx.notify(); + }) + } + }); + + // When any mouse up, stop dragging + window.on_mouse_event({ + let state = self.state.clone(); + let current_ix = state.read(cx).resizing_panel_ix; + let on_resize = self.on_resize.clone(); + move |_: &MouseUpEvent, phase, window, cx| { + if current_ix.is_none() { + return; + } + if phase.bubble() { + state.update(cx, |state, cx| state.done_resizing(cx)); + on_resize(&state, window, cx); + } + } + }) + } +} diff --git a/crates/dock/src/resizable/resize_handle.rs b/crates/dock/src/resizable/resize_handle.rs new file mode 100644 index 0000000..55cda5d --- /dev/null +++ b/crates/dock/src/resizable/resize_handle.rs @@ -0,0 +1,227 @@ +use std::cell::Cell; +use std::rc::Rc; + +use gpui::prelude::FluentBuilder as _; +use gpui::{ + div, px, AnyElement, App, Axis, Element, ElementId, Entity, GlobalElementId, + InteractiveElement, IntoElement, MouseDownEvent, MouseUpEvent, ParentElement as _, Pixels, + Point, Render, StatefulInteractiveElement, Styled as _, Window, +}; +use theme::ActiveTheme; +use ui::AxisExt; + +use crate::dock::DockPlacement; + +pub(crate) const HANDLE_PADDING: Pixels = px(4.); +pub(crate) const HANDLE_SIZE: Pixels = px(1.); + +/// Create a resize handle for a resizable panel. +pub(crate) fn resize_handle( + id: impl Into, + axis: Axis, +) -> ResizeHandle { + ResizeHandle::new(id, axis) +} + +#[allow(clippy::type_complexity)] +pub(crate) struct ResizeHandle { + id: ElementId, + axis: Axis, + drag_value: Option>, + placement: Option, + on_drag: Option, &mut Window, &mut App) -> Entity>>, +} + +impl ResizeHandle { + fn new(id: impl Into, axis: Axis) -> Self { + let id = id.into(); + Self { + id: id.clone(), + on_drag: None, + drag_value: None, + placement: None, + axis, + } + } + + pub(crate) fn on_drag( + mut self, + value: T, + f: impl Fn(Rc, &Point, &mut Window, &mut App) -> Entity + 'static, + ) -> Self { + let value = Rc::new(value); + self.drag_value = Some(value.clone()); + self.on_drag = Some(Rc::new(move |p, window, cx| { + f(value.clone(), p, window, cx) + })); + self + } + + #[allow(dead_code)] + pub(crate) fn placement(mut self, placement: DockPlacement) -> Self { + self.placement = Some(placement); + self + } +} + +#[derive(Default, Debug, Clone)] +struct ResizeHandleState { + active: Cell, +} + +impl ResizeHandleState { + fn set_active(&self, active: bool) { + self.active.set(active); + } + + fn is_active(&self) -> bool { + self.active.get() + } +} + +impl IntoElement for ResizeHandle { + type Element = ResizeHandle; + + fn into_element(self) -> Self::Element { + self + } +} + +impl Element for ResizeHandle { + type PrepaintState = (); + type RequestLayoutState = AnyElement; + + fn id(&self) -> Option { + Some(self.id.clone()) + } + + fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + id: Option<&GlobalElementId>, + _: Option<&gpui::InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (gpui::LayoutId, Self::RequestLayoutState) { + let neg_offset = -HANDLE_PADDING; + let axis = self.axis; + + window.with_element_state(id.unwrap(), |state, window| { + let state = state.unwrap_or(ResizeHandleState::default()); + + let bg_color = if state.is_active() { + cx.theme().border_variant + } else { + cx.theme().border + }; + + let mut el = div() + .id(self.id.clone()) + .occlude() + .absolute() + .flex_shrink_0() + .group("handle") + .when_some(self.on_drag.clone(), |this, on_drag| { + this.on_drag( + self.drag_value.clone().unwrap(), + move |_, position, window, cx| on_drag(&position, window, cx), + ) + }) + .map(|this| match self.placement { + Some(DockPlacement::Left) => { + // Special for Left Dock + // FIXME: Improve this to let the scroll bar have px(HANDLE_PADDING) + this.cursor_col_resize() + .top_0() + .right(px(1.)) + .h_full() + .w(HANDLE_SIZE) + .pl(HANDLE_PADDING) + } + _ => this + .when(axis.is_horizontal(), |this| { + this.cursor_col_resize() + .top_0() + .left(neg_offset) + .h_full() + .w(HANDLE_SIZE) + .px(HANDLE_PADDING) + }) + .when(axis.is_vertical(), |this| { + this.cursor_row_resize() + .top(neg_offset) + .left_0() + .w_full() + .h(HANDLE_SIZE) + .py(HANDLE_PADDING) + }), + }) + .child( + div() + .bg(bg_color) + .group_hover("handle", |this| this.bg(bg_color)) + .when(axis.is_horizontal(), |this| this.h_full().w(HANDLE_SIZE)) + .when(axis.is_vertical(), |this| this.w_full().h(HANDLE_SIZE)), + ) + .into_any_element(); + + let layout_id = el.request_layout(window, cx); + + ((layout_id, el), state) + }) + } + + fn prepaint( + &mut self, + _: Option<&GlobalElementId>, + _: Option<&gpui::InspectorElementId>, + _: gpui::Bounds, + request_layout: &mut Self::RequestLayoutState, + window: &mut Window, + cx: &mut App, + ) -> Self::PrepaintState { + request_layout.prepaint(window, cx); + } + + fn paint( + &mut self, + id: Option<&GlobalElementId>, + _: Option<&gpui::InspectorElementId>, + bounds: gpui::Bounds, + request_layout: &mut Self::RequestLayoutState, + _: &mut Self::PrepaintState, + window: &mut Window, + cx: &mut App, + ) { + request_layout.paint(window, cx); + + window.with_element_state(id.unwrap(), |state: Option, window| { + let state = state.unwrap_or_default(); + + window.on_mouse_event({ + let state = state.clone(); + move |ev: &MouseDownEvent, phase, window, _| { + if bounds.contains(&ev.position) && phase.bubble() { + state.set_active(true); + window.refresh(); + } + } + }); + + window.on_mouse_event({ + let state = state.clone(); + move |_: &MouseUpEvent, _, window, _| { + if state.is_active() { + state.set_active(false); + window.refresh(); + } + } + }); + + ((), state) + }); + } +} diff --git a/crates/dock/src/stack_panel.rs b/crates/dock/src/stack_panel.rs index 6a7a18e..62736a5 100644 --- a/crates/dock/src/stack_panel.rs +++ b/crates/dock/src/stack_panel.rs @@ -1,20 +1,20 @@ use std::sync::Arc; -use gpui::prelude::FluentBuilder; use gpui::{ App, AppContext, Axis, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement, Pixels, Render, SharedString, Styled, Subscription, WeakEntity, Window, }; use smallvec::SmallVec; -use ui::resizable::{ - h_resizable, resizable_panel, v_resizable, ResizablePanel, ResizablePanelEvent, - ResizablePanelGroup, -}; +use theme::{ActiveTheme, CLIENT_SIDE_DECORATION_ROUNDING}; use ui::{h_flex, AxisExt as _, Placement}; use super::{DockArea, PanelEvent}; use crate::panel::{Panel, PanelView}; +use crate::resizable::{ + resizable_panel, ResizablePanelEvent, ResizablePanelGroup, ResizablePanelState, ResizableState, + PANEL_MIN_SIZE, +}; use crate::tab_panel::TabPanel; pub struct StackPanel { @@ -22,9 +22,8 @@ pub struct StackPanel { pub(super) axis: Axis, focus_handle: FocusHandle, pub(crate) panels: SmallVec<[Arc; 2]>, - panel_group: Entity, - #[allow(dead_code)] - subscriptions: Vec, + state: Entity, + _subscriptions: Vec, } impl Panel for StackPanel { @@ -39,28 +38,23 @@ impl Panel for StackPanel { impl StackPanel { pub fn new(axis: Axis, window: &mut Window, cx: &mut Context) -> Self { - let panel_group = cx.new(|cx| { - if axis == Axis::Horizontal { - h_resizable(window, cx) - } else { - v_resizable(window, cx) - } - }); + let state = cx.new(|_| ResizableState::default()); // Bubble up the resize event. - let subscriptions = vec![cx.subscribe_in( - &panel_group, - window, - |_, _, _: &ResizablePanelEvent, _, cx| cx.emit(PanelEvent::LayoutChanged), - )]; + let subscriptions = + vec![ + cx.subscribe_in(&state, window, |_, _, _: &ResizablePanelEvent, _, cx| { + cx.emit(PanelEvent::LayoutChanged) + }), + ]; Self { axis, parent: None, focus_handle: cx.focus_handle(), panels: SmallVec::new(), - panel_group, - subscriptions, + state, + _subscriptions: subscriptions, } } @@ -172,13 +166,6 @@ impl StackPanel { self.insert_panel(panel, ix + 1, size, dock_area, window, cx); } - fn new_resizable_panel(panel: Arc, size: Option) -> ResizablePanel { - resizable_panel() - .content_view(panel.view()) - .content_visible(move |cx| panel.visible(cx)) - .when_some(size, |this, size| this.size(size)) - } - fn insert_panel( &mut self, panel: Arc, @@ -225,14 +212,21 @@ impl StackPanel { ix }; + // Get avg size of all panels to insert new panel, if size is None. + let size = match size { + Some(size) => size, + None => { + let state = self.state.read(cx); + (state.container_size() / (state.sizes().len() + 1) as f32).max(PANEL_MIN_SIZE) + } + }; + + // Insert panel self.panels.insert(ix, panel.clone()); - self.panel_group.update(cx, |view, cx| { - view.insert_child( - Self::new_resizable_panel(panel.clone(), size), - ix, - window, - cx, - ) + + // Update resizable state + self.state.update(cx, |state, cx| { + state.insert_panel(Some(size), Some(ix), cx); }); cx.emit(PanelEvent::LayoutChanged); @@ -240,21 +234,25 @@ impl StackPanel { } /// Remove panel from the stack. + /// + /// If `ix` is not found, do nothing. pub fn remove_panel( &mut self, panel: Arc, window: &mut Window, cx: &mut Context, ) { - if let Some(ix) = self.index_of_panel(panel.clone()) { - self.panels.remove(ix); - self.panel_group.update(cx, |view, cx| { - view.remove_child(ix, window, cx); - }); + let Some(ix) = self.index_of_panel(panel.clone()) else { + return; + }; - cx.emit(PanelEvent::LayoutChanged); - self.remove_self_if_empty(window, cx); - } + self.panels.remove(ix); + self.state.update(cx, |state, cx| { + state.remove_panel(ix, cx); + }); + + cx.emit(PanelEvent::LayoutChanged); + self.remove_self_if_empty(window, cx); } /// Replace the old panel with the new panel at same index. @@ -262,18 +260,14 @@ impl StackPanel { &mut self, old_panel: Arc, new_panel: Entity, - window: &mut Window, + _window: &mut Window, cx: &mut Context, ) { if let Some(ix) = self.index_of_panel(old_panel.clone()) { self.panels[ix] = Arc::new(new_panel.clone()); - self.panel_group.update(cx, |view, cx| { - view.replace_child( - Self::new_resizable_panel(Arc::new(new_panel.clone()), None), - ix, - window, - cx, - ); + let panel_state = ResizablePanelState::default(); + self.state.update(cx, |state, cx| { + state.replace_panel(ix, panel_state, cx); }); cx.emit(PanelEvent::LayoutChanged); } @@ -362,17 +356,17 @@ impl StackPanel { } /// Remove all panels from the stack. - pub(super) fn remove_all_panels(&mut self, window: &mut Window, cx: &mut Context) { + pub(super) fn remove_all_panels(&mut self, _: &mut Window, cx: &mut Context) { self.panels.clear(); - self.panel_group - .update(cx, |view, cx| view.remove_all_children(window, cx)); + self.state.update(cx, |state, cx| { + state.clear(); + cx.notify(); + }); } /// Change the axis of the stack panel. - pub(super) fn set_axis(&mut self, axis: Axis, window: &mut Window, cx: &mut Context) { + pub(super) fn set_axis(&mut self, axis: Axis, _: &mut Window, cx: &mut Context) { self.axis = axis; - self.panel_group - .update(cx, |view, cx| view.set_axis(axis, window, cx)); cx.notify(); } } @@ -388,10 +382,21 @@ impl EventEmitter for StackPanel {} impl EventEmitter for StackPanel {} impl Render for StackPanel { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { h_flex() .size_full() .overflow_hidden() - .child(self.panel_group.clone()) + .rounded(CLIENT_SIDE_DECORATION_ROUNDING) + .bg(cx.theme().elevated_surface_background) + .child( + ResizablePanelGroup::new("stack-panel-group") + .with_state(&self.state) + .axis(self.axis) + .children(self.panels.clone().into_iter().map(|panel| { + resizable_panel() + .child(panel.view()) + .visible(panel.visible(cx)) + })), + ) } } diff --git a/crates/ui/src/tab/mod.rs b/crates/dock/src/tab/mod.rs similarity index 75% rename from crates/ui/src/tab/mod.rs rename to crates/dock/src/tab/mod.rs index 572b7d8..baa2d69 100644 --- a/crates/ui/src/tab/mod.rs +++ b/crates/dock/src/tab/mod.rs @@ -1,38 +1,45 @@ use gpui::prelude::FluentBuilder; use gpui::{ - div, px, AnyElement, App, Div, ElementId, InteractiveElement, IntoElement, ParentElement, - RenderOnce, Stateful, StatefulInteractiveElement, Styled, Window, + div, px, AnyElement, App, Div, InteractiveElement, IntoElement, ParentElement, RenderOnce, + StatefulInteractiveElement, Styled, Window, }; use theme::ActiveTheme; - -use crate::Selectable; +use ui::{Selectable, Sizable, Size}; pub mod tab_bar; #[derive(IntoElement)] pub struct Tab { - base: Stateful
, - label: AnyElement, + ix: usize, + base: Div, + label: Option, prefix: Option, suffix: Option, disabled: bool, selected: bool, + size: Size, } impl Tab { - pub fn new(id: impl Into, label: impl IntoElement) -> Self { - let id: ElementId = id.into(); - + pub fn new() -> Self { Self { - base: div().id(id), - label: label.into_any_element(), + ix: 0, + base: div(), + label: None, disabled: false, selected: false, prefix: None, suffix: None, + size: Size::default(), } } + /// Set label for the tab. + pub fn label(mut self, label: impl Into) -> Self { + self.label = Some(label.into()); + self + } + /// Set the left side of the tab pub fn prefix(mut self, prefix: impl Into) -> Self { self.prefix = Some(prefix.into()); @@ -50,6 +57,18 @@ impl Tab { self.disabled = disabled; self } + + /// Set index to the tab. + pub fn ix(mut self, ix: usize) -> Self { + self.ix = ix; + self + } +} + +impl Default for Tab { + fn default() -> Self { + Self::new() + } } impl Selectable for Tab { @@ -77,6 +96,13 @@ impl Styled for Tab { } } +impl Sizable for Tab { + fn with_size(mut self, size: impl Into) -> Self { + self.size = size.into(); + self + } +} + impl RenderOnce for Tab { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { let (text_color, bg_color, hover_bg_color) = match (self.selected, self.disabled) { @@ -103,6 +129,7 @@ impl RenderOnce for Tab { }; self.base + .id(self.ix) .h(px(30.)) .px_2() .relative() @@ -115,12 +142,11 @@ impl RenderOnce for Tab { .text_ellipsis() .text_color(text_color) .bg(bg_color) - .rounded(cx.theme().radius_lg) .hover(|this| this.bg(hover_bg_color)) .when_some(self.prefix, |this, prefix| { this.child(prefix).text_color(text_color) }) - .child(self.label) + .when_some(self.label, |this, label| this.child(label)) .when_some(self.suffix, |this, suffix| this.child(suffix)) } } diff --git a/crates/dock/src/tab/tab_bar.rs b/crates/dock/src/tab/tab_bar.rs new file mode 100644 index 0000000..8489f50 --- /dev/null +++ b/crates/dock/src/tab/tab_bar.rs @@ -0,0 +1,143 @@ +use gpui::prelude::FluentBuilder as _; +#[cfg(not(target_os = "windows"))] +use gpui::Pixels; +use gpui::{ + div, px, AnyElement, App, Div, InteractiveElement, IntoElement, ParentElement, RenderOnce, + ScrollHandle, StatefulInteractiveElement as _, StyleRefinement, Styled, Window, +}; +use smallvec::SmallVec; +use theme::ActiveTheme; +use ui::{h_flex, Sizable, Size, StyledExt}; + +use crate::platforms::linux::LinuxWindowControls; +use crate::platforms::windows::WindowsWindowControls; + +#[derive(IntoElement)] +pub struct TabBar { + base: Div, + style: StyleRefinement, + scroll_handle: Option, + prefix: Option, + suffix: Option, + last_empty_space: AnyElement, + children: SmallVec<[AnyElement; 2]>, + size: Size, +} + +impl TabBar { + pub fn new() -> Self { + Self { + base: div().px(px(-1.)), + style: StyleRefinement::default(), + scroll_handle: None, + children: SmallVec::new(), + prefix: None, + suffix: None, + size: Size::default(), + last_empty_space: div().w_3().into_any_element(), + } + } + + /// Track the scroll of the TabBar. + pub fn track_scroll(mut self, scroll_handle: &ScrollHandle) -> Self { + self.scroll_handle = Some(scroll_handle.clone()); + self + } + + /// Set the prefix element of the TabBar + pub fn prefix(mut self, prefix: impl IntoElement) -> Self { + self.prefix = Some(prefix.into_any_element()); + self + } + + /// Set the suffix element of the TabBar + pub fn suffix(mut self, suffix: impl IntoElement) -> Self { + self.suffix = Some(suffix.into_any_element()); + self + } + + /// Set the last empty space element of the TabBar. + pub fn last_empty_space(mut self, last_empty_space: impl IntoElement) -> Self { + self.last_empty_space = last_empty_space.into_any_element(); + self + } + + #[cfg(not(target_os = "windows"))] + pub fn height(window: &mut Window) -> Pixels { + (1.75 * window.rem_size()).max(px(36.)) + } +} + +impl Default for TabBar { + fn default() -> Self { + Self::new() + } +} + +impl ParentElement for TabBar { + fn extend(&mut self, elements: impl IntoIterator) { + self.children.extend(elements) + } +} + +impl Styled for TabBar { + fn style(&mut self) -> &mut StyleRefinement { + &mut self.style + } +} + +impl Sizable for TabBar { + fn with_size(mut self, size: impl Into) -> Self { + self.size = size.into(); + self + } +} + +impl RenderOnce for TabBar { + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { + let focused = window.is_window_active(); + + self.base + .group("tab-bar") + .refine_style(&self.style) + .relative() + .flex() + .items_center() + .bg(cx.theme().elevated_surface_background) + .when(!focused, |this| { + this.bg(cx.theme().elevated_surface_background.opacity(0.75)) + }) + .border_b_1() + .border_color(cx.theme().border) + .overflow_hidden() + .text_color(cx.theme().text) + .when_some(self.prefix, |this, prefix| this.child(prefix)) + .child( + h_flex() + .id("tabs") + .flex_grow() + .gap_1() + .overflow_x_scroll() + .when_some(self.scroll_handle, |this, scroll_handle| { + this.track_scroll(&scroll_handle) + }) + .children(self.children) + .when(self.suffix.is_some(), |this| { + this.child(self.last_empty_space) + }), + ) + .when_some(self.suffix, |this, suffix| this.child(suffix)) + .when( + !cx.theme().platform.is_mac() && !window.is_fullscreen(), + |this| match cx.theme().platform { + theme::PlatformKind::Linux => { + this.child(div().px_2().child(LinuxWindowControls::new())) + } + theme::PlatformKind::Windows => { + this.child(WindowsWindowControls::new(Self::height(window))) + } + _ => this, + }, + ) + } +} diff --git a/crates/dock/src/tab_panel.rs b/crates/dock/src/tab_panel.rs index 0ef0efd..68c984c 100644 --- a/crates/dock/src/tab_panel.rs +++ b/crates/dock/src/tab_panel.rs @@ -5,17 +5,18 @@ use gpui::{ div, px, rems, App, AppContext, Context, Corner, DefiniteLength, DismissEvent, DragMoveEvent, Empty, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement as _, IntoElement, MouseButton, ParentElement, Pixels, Render, ScrollHandle, SharedString, - StatefulInteractiveElement, Styled, WeakEntity, Window, + StatefulInteractiveElement, Styled, WeakEntity, Window, WindowControlArea, }; -use theme::ActiveTheme; +use theme::{ActiveTheme, PlatformKind, CLIENT_SIDE_DECORATION_ROUNDING, TITLEBAR_HEIGHT}; use ui::button::{Button, ButtonVariants as _}; use ui::popup_menu::{PopupMenu, PopupMenuExt}; -use ui::tab::tab_bar::TabBar; -use ui::tab::Tab; use ui::{h_flex, v_flex, AxisExt, IconName, Placement, Selectable, Sizable, StyledExt}; +use crate::dock::DockPlacement; use crate::panel::{Panel, PanelView}; use crate::stack_panel::StackPanel; +use crate::tab::tab_bar::TabBar; +use crate::tab::Tab; use crate::{ClosePanel, DockArea, PanelEvent, PanelStyle, ToggleZoom}; #[derive(Clone)] @@ -64,16 +65,32 @@ impl Render for DragPanel { pub struct TabPanel { focus_handle: FocusHandle, dock_area: WeakEntity, - /// The stock_panel can be None, if is None, that means the panels can't be split or move - stack_panel: Option>, + + /// List of panels in the tab panel pub(crate) panels: Vec>, + + /// Current active panel index pub(crate) active_ix: usize, + /// If this is true, the Panel closeable will follow the active panel's closeable, /// otherwise this TabPanel will not able to close pub(crate) closable: bool, + + /// The stock_panel can be None, if is None, that means the panels can't be split or move + stack_panel: Option>, + + /// Scroll handle for the tab bar tab_bar_scroll_handle: ScrollHandle, - is_zoomed: bool, - is_collapsed: bool, + + /// Whether the tab panel is zoomeds + zoomed: bool, + + /// Whether the tab panel is collapsed + collapsed: bool, + + /// Whether window is moving + window_move: bool, + /// When drag move, will get the placement of the panel to be split will_split_placement: Option, } @@ -141,8 +158,9 @@ impl TabPanel { active_ix: 0, tab_bar_scroll_handle: ScrollHandle::new(), will_split_placement: None, - is_zoomed: false, - is_collapsed: false, + zoomed: false, + collapsed: false, + window_move: false, closable: true, } } @@ -338,7 +356,7 @@ impl TabPanel { _window: &mut Window, cx: &mut Context, ) { - self.is_collapsed = collapsed; + self.collapsed = collapsed; cx.notify(); } @@ -351,7 +369,7 @@ impl TabPanel { return true; } - if self.is_zoomed { + if self.zoomed { return true; } @@ -407,7 +425,7 @@ impl TabPanel { window: &mut Window, cx: &mut Context, ) -> impl IntoElement { - let is_zoomed = self.is_zoomed && state.zoomable; + let is_zoomed = self.zoomed && state.zoomable; let view = cx.entity().clone(); let build_popup_menu = move |this, cx: &App| view.read(cx).popup_menu(this, cx); let toolbar = self.toolbar_buttons(window, cx); @@ -419,7 +437,7 @@ impl TabPanel { .occlude() .rounded_full() .children(toolbar.into_iter().map(|btn| btn.small().ghost().rounded())) - .when(self.is_zoomed, |this| { + .when(self.zoomed, |this| { this.child( Button::new("zoom") .icon(IconName::Zoom) @@ -432,8 +450,7 @@ impl TabPanel { ) }) .when(has_toolbar, |this| { - this.bg(cx.theme().surface_background) - .child(div().flex_shrink_0().h_4().w_px().bg(cx.theme().border)) + this.child(div().flex_shrink_0().h_4().w_px().bg(cx.theme().border)) }) .child( Button::new("menu") @@ -460,21 +477,115 @@ impl TabPanel { ) } + fn render_dock_toggle_button( + &self, + placement: DockPlacement, + _window: &mut Window, + cx: &mut Context, + ) -> Option