From 7118565b4ddcfcfb9a5bb58b39c052fbd17b4c23 Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Thu, 17 Sep 2026 15:50:53 +0700 Subject: [PATCH] clean up --- Cargo.lock | 973 ++++++++++++++++----------- Cargo.toml | 23 +- crates/gpui_tokio/Cargo.toml | 11 + crates/gpui_tokio/src/lib.rs | 102 +++ crates/theme/Cargo.toml | 1 + crates/theme/src/lib.rs | 61 ++ crates/ui/Cargo.toml | 3 +- crates/ui/src/checkbox.rs | 312 --------- crates/ui/src/element_ext.rs | 27 - crates/ui/src/event.rs | 21 - crates/ui/src/focusable.rs | 39 -- crates/ui/src/index_path.rs | 69 -- crates/ui/src/lib.rs | 14 +- crates/ui/src/list/cache.rs | 221 ------ crates/ui/src/list/delegate.rs | 171 ----- crates/ui/src/list/list.rs | 747 -------------------- crates/ui/src/list/list_item.rs | 226 ------- crates/ui/src/list/loading.rs | 34 - crates/ui/src/list/mod.rs | 28 - crates/ui/src/list/separator_item.rs | 50 -- crates/ui/src/styled.rs | 27 +- docs/gpui-base-migration.md | 218 ++++-- web/Cargo.toml | 2 +- 23 files changed, 942 insertions(+), 2438 deletions(-) create mode 100644 crates/gpui_tokio/Cargo.toml create mode 100644 crates/gpui_tokio/src/lib.rs delete mode 100644 crates/ui/src/checkbox.rs delete mode 100644 crates/ui/src/element_ext.rs delete mode 100644 crates/ui/src/event.rs delete mode 100644 crates/ui/src/focusable.rs delete mode 100644 crates/ui/src/index_path.rs delete mode 100644 crates/ui/src/list/cache.rs delete mode 100644 crates/ui/src/list/delegate.rs delete mode 100644 crates/ui/src/list/list.rs delete mode 100644 crates/ui/src/list/list_item.rs delete mode 100644 crates/ui/src/list/loading.rs delete mode 100644 crates/ui/src/list/mod.rs delete mode 100644 crates/ui/src/list/separator_item.rs diff --git a/Cargo.lock b/Cargo.lock index 75038728..d8ad9f99 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -270,7 +270,7 @@ name = "assets" version = "1.0.2" dependencies = [ "anyhow", - "gpui", + "gpui-pre", "log", "rust-embed", ] @@ -531,7 +531,7 @@ dependencies = [ name = "auto_update" version = "1.0.2" dependencies = [ - "gpui", + "gpui-pre", "gpui-updater-core", "instant", "log", @@ -1072,7 +1072,7 @@ dependencies = [ "flume 0.11.1", "futures", "fuzzy-matcher", - "gpui", + "gpui-pre", "instant", "itertools 0.13.0", "log", @@ -1093,7 +1093,7 @@ dependencies = [ "common", "flume 0.11.1", "futures", - "gpui", + "gpui-pre", "itertools 0.13.0", "linkify", "log", @@ -1241,16 +1241,6 @@ dependencies = [ "unicode-width", ] -[[package]] -name = "collections" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" -dependencies = [ - "gpui_util", - "indexmap", - "rustc-hash 2.1.3", -] - [[package]] name = "color_quant" version = "1.1.0" @@ -1272,9 +1262,9 @@ name = "common" version = "1.0.2" dependencies = [ "chrono", - "dirs 5.0.1", + "dirs", "futures", - "gpui", + "gpui-pre", "instant", "itertools 0.13.0", "log", @@ -1423,15 +1413,15 @@ dependencies = [ "chat", "common", "device", - "gpui", - "gpui_linux", - "gpui_macos", - "gpui_platform", - "gpui_windows", + "gpui-pre", + "gpui-pre-linux", + "gpui-pre-macos", + "gpui-pre-platform", + "gpui-pre-reqwest-client", + "gpui-pre-windows", "log", "nostr-sdk", "person", - "reqwest_client", "settings", "state", "theme", @@ -1453,9 +1443,9 @@ dependencies = [ "getrandom 0.2.17", "getrandom 0.3.4", "getrandom 0.4.3", - "gpui", - "gpui_platform", - "gpui_web", + "gpui-pre", + "gpui-pre-platform", + "gpui-pre-web", "instant", "log", "person", @@ -1790,16 +1780,6 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "derive_refineable" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "device" version = "1.0.2" @@ -1807,7 +1787,7 @@ dependencies = [ "anyhow", "common", "flume 0.11.1", - "gpui", + "gpui-pre", "instant", "log", "nostr-sdk", @@ -1849,16 +1829,7 @@ version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" dependencies = [ - "dirs-sys 0.4.1", -] - -[[package]] -name = "dirs" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" -dependencies = [ - "dirs-sys 0.5.0", + "dirs-sys", ] [[package]] @@ -1869,22 +1840,10 @@ checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ "libc", "option-ext", - "redox_users 0.4.6", + "redox_users", "windows-sys 0.48.0", ] -[[package]] -name = "dirs-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" -dependencies = [ - "libc", - "option-ext", - "redox_users 0.5.3", - "windows-sys 0.61.2", -] - [[package]] name = "dispatch" version = "0.2.0" @@ -2387,6 +2346,16 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + [[package]] name = "futures" version = "0.3.34" @@ -2704,9 +2673,45 @@ dependencies = [ ] [[package]] -name = "gpui" -version = "0.2.2" -source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" +name = "gpui-base" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d45dcaaeac889bf1e7757db1beb26c9043c8ea3156651facc11c6be56bb6722" +dependencies = [ + "aho-corasick", + "anyhow", + "async-channel", + "chrono", + "futures", + "gpui-pre", + "gpui-pre-macros", + "gpui-pre-sum-tree", + "html5ever", + "instant", + "lsp-types", + "markdown", + "markup5ever_rcdom", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", + "raw-window-handle", + "regex", + "ropey", + "schemars", + "serde", + "serde_json", + "smallvec", + "smol", + "tracing", + "unicode-segmentation", + "web-time", +] + +[[package]] +name = "gpui-pre" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a9ca98fdcad06276de623c2f48ebd710f1e7724eebef2f2673c583e85dc8335" dependencies = [ "accesskit", "anyhow", @@ -2715,7 +2720,6 @@ dependencies = [ "bindgen", "bitflags 2.13.2", "chrono", - "collections", "core-video", "ctor", "derive_more", @@ -2724,19 +2728,23 @@ dependencies = [ "futures", "futures-concurrency", "getrandom 0.3.4", - "gpui_macros", - "gpui_shared_string", - "gpui_util", + "gpui-pre-collections", + "gpui-pre-http-client", + "gpui-pre-macros", + "gpui-pre-refineable", + "gpui-pre-scheduler", + "gpui-pre-shared-string", + "gpui-pre-sum-tree", + "gpui-pre-util", + "gpui-pre-util-macros", + "gpui-pre-ztracing", "heapless 0.9.3", - "http_client", "image", "inventory", "itertools 0.14.0", "log", "lyon", "num_cpus", - "objc2-core-foundation", - "objc2-core-video", "parking", "parking_lot", "pin-project", @@ -2745,10 +2753,8 @@ dependencies = [ "profiling", "rand 0.9.5", "raw-window-handle", - "refineable", "regex", "resvg", - "scheduler", "schemars", "seahash", "serde", @@ -2757,13 +2763,12 @@ dependencies = [ "smallvec", "spin 0.10.1", "strum", - "sum_tree", "taffy", "thiserror 2.0.20", + "tracing", "ttf-parser", "url", "usvg", - "util_macros", "uuid", "waker-fn", "web-time", @@ -2773,36 +2778,22 @@ dependencies = [ ] [[package]] -name = "gpui-updater-core" -version = "0.1.0" -source = "git+https://github.com/AprilNEA/gpui-updater#a622818f581eb8663a3a19d65f18b962f665946c" -dependencies = [ - "minisign-verify", - "semver", - "serde", - "serde_json", - "sha2 0.10.9", - "thiserror 2.0.20", - "tracing", - "ureq", -] - -[[package]] -name = "gpui_apple" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" +name = "gpui-pre-apple" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fc4da82c5c2dbb25443655cfbc4305a2e2d1380bc0b815e80664fdfbf4cfdb" dependencies = [ "anyhow", "block", "cbindgen", "cocoa 0.26.0", - "collections", "core-foundation 0.10.1", "core-video", "derive_more", "etagere", "foreign-types", - "gpui", + "gpui-pre", + "gpui-pre-collections", "image", "log", "metal", @@ -2812,9 +2803,65 @@ dependencies = [ ] [[package]] -name = "gpui_linux" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" +name = "gpui-pre-collections" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626a09f683ff6f3b2f708341bdcea9d479af5b14a6a2fccec9cff935ec0751b7" +dependencies = [ + "gpui-pre-util", + "indexmap", + "rustc-hash 2.1.3", +] + +[[package]] +name = "gpui-pre-derive-refineable" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56d06116a2b857588f6ea7bfb447b3554ba985e780b5995ae47780252da152f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "gpui-pre-http-client" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0d2071c582c1a0f71b8c7c00607a3b9cd18f81fb3510d41845db3447b2a11c9" +dependencies = [ + "anyhow", + "async-compression", + "bytes", + "derive_more", + "futures", + "http", + "http-body", + "log", + "parking_lot", + "serde", + "serde_json", + "serde_urlencoded", + "url", +] + +[[package]] +name = "gpui-pre-http-client-tls" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b6f3936802580259d5af9bcb2ab37fb0d7002caeed449eaabb007c62aea7341" +dependencies = [ + "log", + "rustls", + "rustls-platform-verifier", + "webpki-roots 1.0.9", +] + +[[package]] +name = "gpui-pre-linux" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b427589681ec1fed27951fd82edc70a7e288bb2487ede26dbbd07c2ce23d4f" dependencies = [ "accesskit", "accesskit_unix", @@ -2825,13 +2872,13 @@ dependencies = [ "bytemuck", "calloop", "calloop-wayland-source", - "collections", "filedescriptor", "futures", - "gpui", - "gpui_util", - "gpui_wgpu", - "http_client", + "gpui-pre", + "gpui-pre-collections", + "gpui-pre-http-client", + "gpui-pre-util", + "gpui-pre-wgpu", "libc", "log", "notify-rust", @@ -2858,17 +2905,18 @@ dependencies = [ ] [[package]] -name = "gpui_macos" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" +name = "gpui-pre-macos" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "806eac0cb5c0eebc0b032c6450c11d283264f9ac05754dda83fc80d0b86b4ee8" dependencies = [ "accesskit", "accesskit_macos", "anyhow", "async-task", + "block", "block2 0.6.2", "cocoa 0.26.0", - "collections", "core-foundation 0.10.1", "core-foundation-sys", "core-graphics 0.24.0", @@ -2877,19 +2925,20 @@ dependencies = [ "dispatch2", "foreign-types", "futures", - "gpui", - "gpui_apple", - "gpui_util", + "gpui-pre", + "gpui-pre-apple", + "gpui-pre-collections", + "gpui-pre-media", + "gpui-pre-util", "image", "itertools 0.14.0", "libc", "log", "mach2", + "metal", "objc", "objc2 0.6.4", "objc2-app-kit 0.3.2", - "objc2-core-graphics", - "objc2-core-media", "objc2-foundation 0.3.2", "objc2-screen-capture-kit", "objc2-user-notifications", @@ -2904,33 +2953,157 @@ dependencies = [ ] [[package]] -name = "gpui_macros" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" +name = "gpui-pre-macros" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06139051bf1db51949bfbb4ee338208bd64b4c35340d8c1a6a8200744e28fe1" dependencies = [ "heck 0.5.0", + "proc-macro-crate", "proc-macro2", "quote", "syn 2.0.119", ] [[package]] -name = "gpui_platform" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" +name = "gpui-pre-media" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "500acb13f818ff234021c1b9eae8592b0cbe017a64e26908c736b7421b1c6213" dependencies = [ - "console_error_panic_hook", - "gpui", - "gpui_linux", - "gpui_macos", - "gpui_web", - "gpui_windows", + "anyhow", + "bindgen", + "core-foundation 0.10.1", + "core-video", + "foreign-types", + "metal", + "objc", ] [[package]] -name = "gpui_shared_string" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" +name = "gpui-pre-perf" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0973bbb02bf46940ac0861d9dce0fc03a3109634172b69108d1bc13fa29d76c" +dependencies = [ + "gpui-pre-collections", + "serde", + "serde_json", +] + +[[package]] +name = "gpui-pre-platform" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b613512e29f1e7b825bfa7e50a447388ff1347044350a475e4c98a892a985c8d" +dependencies = [ + "console_error_panic_hook", + "gpui-pre", + "gpui-pre-linux", + "gpui-pre-macos", + "gpui-pre-web", + "gpui-pre-windows", +] + +[[package]] +name = "gpui-pre-refineable" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "428f23703ead5601618ffea73c137897b98a94ce8d30e257f22be947732dfbf9" +dependencies = [ + "gpui-pre-derive-refineable", +] + +[[package]] +name = "gpui-pre-reqwest" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05be23908e707966824f8c51a609904b7f30739e8d915e120a53c1b995ba964c" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "mime_guess", + "once_cell", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-native-certs", + "rustls-pemfile", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-rustls", + "tokio-socks", + "tokio-util", + "tower", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "windows-registry 0.4.0", +] + +[[package]] +name = "gpui-pre-reqwest-client" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05d4e3d2eb2ab777597879e46047247447b9d93cef568e63d545c9deb1b3e57" +dependencies = [ + "anyhow", + "bytes", + "futures", + "gpui-pre-http-client", + "gpui-pre-http-client-tls", + "gpui-pre-reqwest", + "gpui-pre-util", + "log", + "regex", + "tokio", +] + +[[package]] +name = "gpui-pre-scheduler" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd1b7ff2e7bf43302ae982cd786a22c16c52e16682e67b66cfbe2cb2bc3a72d" +dependencies = [ + "async-task", + "backtrace", + "chrono", + "flume 0.12.0", + "futures", + "parking_lot", + "rand 0.9.5", + "wasm_thread", + "web-time", +] + +[[package]] +name = "gpui-pre-shared-string" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "424a4dd6a570473d57acf1df2bfb9f3028d4b568abcaeb9252e0f13047b50c03" dependencies = [ "schemars", "serde", @@ -2938,20 +3111,23 @@ dependencies = [ ] [[package]] -name = "gpui_tokio" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" +name = "gpui-pre-sum-tree" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4855718e82630d29198b515d2b027361ef63dd19b6021cefe945789dcff82372" dependencies = [ - "anyhow", - "gpui", - "gpui_util", - "tokio", + "gpui-pre-ztracing", + "heapless 0.9.3", + "log", + "rayon", + "tracing", ] [[package]] -name = "gpui_util" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" +name = "gpui-pre-util" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e973148165346c0f9b3f5072b321d7c2f33807c93daea67fe298c005c0e0c2a8" dependencies = [ "anyhow", "log", @@ -2959,21 +3135,33 @@ dependencies = [ ] [[package]] -name = "gpui_web" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" +name = "gpui-pre-util-macros" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f71f6212d3772a70988c7d64627c7284dec73c17992ac4f9f0cdfdd46cbb677" +dependencies = [ + "gpui-pre-perf", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "gpui-pre-web" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f0daada903f106be946d87e25f5bc85ad6cf60d6d82cb8f8aed945417a9c1a3" dependencies = [ "anyhow", "console_error_panic_hook", "futures", - "gpui", - "gpui_wgpu", - "http_client", + "gpui-pre", + "gpui-pre-http-client", + "gpui-pre-scheduler", + "gpui-pre-wgpu", "js-sys", "log", "parking_lot", "raw-window-handle", - "scheduler", "smallvec", "unicode-properties", "unicode-script", @@ -2987,17 +3175,18 @@ dependencies = [ ] [[package]] -name = "gpui_wgpu" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" +name = "gpui-pre-wgpu" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "640b666a16ddf9504e2eb3e7a997acb2b2adfaa7859925bdd36fc3acfd8f30a7" dependencies = [ "anyhow", "bytemuck", - "collections", "cosmic-text", "etagere", - "gpui", - "gpui_util", + "gpui-pre", + "gpui-pre-collections", + "gpui-pre-util", "itertools 0.14.0", "log", "parking_lot", @@ -3013,19 +3202,20 @@ dependencies = [ ] [[package]] -name = "gpui_windows" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" +name = "gpui-pre-windows" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c12c7d84ec422feb2f064117d572447998381e5c219fcad69fe19f10d0b7aaea" dependencies = [ "accesskit", "accesskit_windows", "anyhow", - "collections", "dunce", "etagere", "futures", - "gpui", - "gpui_util", + "gpui-pre", + "gpui-pre-collections", + "gpui-pre-util", "image", "itertools 0.14.0", "log", @@ -3040,6 +3230,61 @@ dependencies = [ "windows-registry 0.6.1", ] +[[package]] +name = "gpui-pre-zlog" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98352858fac3d4d05b46d6014ad04bad313652a691d4e21dc55fa96fb4acb7cf" +dependencies = [ + "anyhow", + "chrono", + "gpui-pre-collections", + "log", +] + +[[package]] +name = "gpui-pre-ztracing" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d63a3c7276875b4d7e6d3961e92a138e9233e86a591bf318f2e54df4ad58102" +dependencies = [ + "gpui-pre-zlog", + "gpui-pre-ztracing-macro", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "gpui-pre-ztracing-macro" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84e2a5ff79002223edfcfa76c6f9f79f4f60f6ec545ccc27832f12bd46348b39" + +[[package]] +name = "gpui-updater-core" +version = "0.1.0" +source = "git+https://github.com/AprilNEA/gpui-updater#a622818f581eb8663a3a19d65f18b962f665946c" +dependencies = [ + "minisign-verify", + "semver", + "serde", + "serde_json", + "sha2 0.10.9", + "thiserror 2.0.20", + "tracing", + "ureq", +] + +[[package]] +name = "gpui_tokio" +version = "0.1.0" +dependencies = [ + "anyhow", + "gpui-pre", + "gpui-pre-util", + "tokio", +] + [[package]] name = "h2" version = "0.4.19" @@ -3254,6 +3499,20 @@ dependencies = [ "digest 0.11.3", ] +[[package]] +name = "html5ever" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "http" version = "1.5.0" @@ -3287,37 +3546,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "http_client" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" -dependencies = [ - "anyhow", - "async-compression", - "bytes", - "derive_more", - "futures", - "http", - "http-body", - "log", - "parking_lot", - "serde", - "serde_json", - "serde_urlencoded", - "url", -] - -[[package]] -name = "http_client_tls" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" -dependencies = [ - "log", - "rustls", - "rustls-platform-verifier", - "webpki-roots 1.0.9", -] - [[package]] name = "httparse" version = "1.10.1" @@ -4034,6 +4262,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + [[package]] name = "mac-notification-sys" version = "0.6.15" @@ -4066,6 +4300,42 @@ dependencies = [ "libc", ] +[[package]] +name = "markdown" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5cab8f2cadc416a82d2e783a1946388b31654d391d1c7d92cc1f03e295b1deb" +dependencies = [ + "serde", + "unicode-id", +] + +[[package]] +name = "markup5ever" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45" +dependencies = [ + "log", + "phf 0.11.3", + "phf_codegen", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "markup5ever_rcdom" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edaa21ab3701bfee5099ade5f7e1f84553fd19228cf332f13cd6e964bf59be18" +dependencies = [ + "html5ever", + "markup5ever", + "tendril", + "xml5ever", +] + [[package]] name = "maybe-rayon" version = "0.1.1" @@ -4645,8 +4915,8 @@ dependencies = [ "block2 0.5.1", "libc", "objc2 0.5.2", - "objc2-core-data", - "objc2-core-image", + "objc2-core-data 0.2.2", + "objc2-core-image 0.2.2", "objc2-foundation 0.2.2", "objc2-quartz-core 0.2.2", ] @@ -4659,31 +4929,28 @@ checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ "bitflags 2.13.2", "block2 0.6.2", + "libc", "objc2 0.6.4", + "objc2-cloud-kit", + "objc2-core-data 0.3.2", "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image 0.3.2", + "objc2-core-text", + "objc2-core-video", "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", ] [[package]] -name = "objc2-core-audio" +name = "objc2-cloud-kit" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2" -dependencies = [ - "dispatch2", - "objc2 0.6.4", - "objc2-core-audio-types", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-core-audio-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" dependencies = [ "bitflags 2.13.2", "objc2 0.6.4", + "objc2-foundation 0.3.2", ] [[package]] @@ -4698,6 +4965,17 @@ dependencies = [ "objc2-foundation 0.2.2", ] +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + [[package]] name = "objc2-core-foundation" version = "0.3.2" @@ -4734,6 +5012,16 @@ dependencies = [ "objc2-metal 0.2.2", ] +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + [[package]] name = "objc2-core-location" version = "0.3.2" @@ -4745,18 +5033,15 @@ dependencies = [ ] [[package]] -name = "objc2-core-media" +name = "objc2-core-text" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ec576860167a15dd9fce7fbee7512beb4e31f532159d3482d1f9c6caedf31d" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" dependencies = [ "bitflags 2.13.2", - "dispatch2", "objc2 0.6.4", - "objc2-core-audio", - "objc2-core-audio-types", "objc2-core-foundation", - "objc2-core-video", + "objc2-core-graphics", ] [[package]] @@ -4871,10 +5156,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74b7c5390f477482f001bc354d6571a70db7e4f8d5288e860c45521fbce11394" dependencies = [ "block2 0.6.2", - "dispatch2", "objc2 0.6.4", "objc2-core-graphics", - "objc2-core-media", "objc2-foundation 0.3.2", ] @@ -5114,16 +5397,6 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" -[[package]] -name = "perf" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" -dependencies = [ - "collections", - "serde", - "serde_json", -] - [[package]] name = "person" version = "1.0.2" @@ -5131,7 +5404,7 @@ dependencies = [ "anyhow", "common", "flume 0.11.1", - "gpui", + "gpui-pre", "instant", "log", "nostr-sdk", @@ -5160,6 +5433,16 @@ dependencies = [ "serde", ] +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", +] + [[package]] name = "phf_generator" version = "0.11.3" @@ -5413,6 +5696,12 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "presser" version = "0.3.1" @@ -5866,16 +6155,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "redox_users" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60dc65c0ff1a7ae1294b0c67b9f14baf70b644404010370171787bfac1038fc0" -dependencies = [ - "libredox", - "thiserror 2.0.20", -] - [[package]] name = "ref-cast" version = "1.0.27" @@ -5896,14 +6175,6 @@ dependencies = [ "syn 3.0.6", ] -[[package]] -name = "refineable" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" -dependencies = [ - "derive_refineable", -] - [[package]] name = "regex" version = "1.13.1" @@ -5978,23 +6249,6 @@ dependencies = [ "webpki-roots 1.0.9", ] -[[package]] -name = "reqwest_client" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" -dependencies = [ - "anyhow", - "bytes", - "futures", - "gpui_util", - "http_client", - "http_client_tls", - "log", - "regex", - "tokio", - "zed-reqwest", -] - [[package]] name = "resvg" version = "0.46.0" @@ -6279,22 +6533,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "scheduler" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" -dependencies = [ - "async-task", - "backtrace", - "chrono", - "flume 0.12.0", - "futures", - "parking_lot", - "rand 0.9.5", - "wasm_thread", - "web-time", -] - [[package]] name = "schemars" version = "1.2.2" @@ -6554,7 +6792,7 @@ version = "1.0.2" dependencies = [ "anyhow", "common", - "gpui", + "gpui-pre", "log", "nostr-sdk", "paste", @@ -6803,7 +7041,7 @@ dependencies = [ "data-encoding", "flume 0.11.1", "futures", - "gpui", + "gpui-pre", "gpui_tokio", "instant", "log", @@ -6849,6 +7087,31 @@ dependencies = [ "float-cmp", ] +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.11.3", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", +] + [[package]] name = "strum" version = "0.28.0" @@ -6876,18 +7139,6 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" -[[package]] -name = "sum_tree" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" -dependencies = [ - "heapless 0.9.3", - "log", - "rayon", - "tracing", - "ztracing", -] - [[package]] name = "sval" version = "2.22.0" @@ -7070,12 +7321,12 @@ dependencies = [ [[package]] name = "system-configuration" -version = "0.8.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501336eb7ba9e417300a6a0fa985721065467aa83a6dcf0422a8e43e4c0328fa" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ "bitflags 2.13.2", - "core-foundation 0.10.1", + "core-foundation 0.9.4", "system-configuration-sys", ] @@ -7137,6 +7388,17 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -7151,7 +7413,8 @@ name = "theme" version = "1.0.2" dependencies = [ "anyhow", - "gpui", + "gpui-base", + "gpui-pre", "log", "schemars", "serde", @@ -7692,7 +7955,9 @@ version = "1.0.2" dependencies = [ "anyhow", "common", - "gpui", + "gpui-base", + "gpui-pre", + "gpui-pre-sum-tree", "instant", "itertools 0.13.0", "log", @@ -7702,7 +7967,6 @@ dependencies = [ "serde", "smallvec", "smol", - "sum_tree", "theme", "tree-sitter", "unicode-segmentation", @@ -7733,6 +7997,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" +[[package]] +name = "unicode-id" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ba288e709927c043cbe476718d37be306be53fb1fafecd0dbe36d072be2580" + [[package]] name = "unicode-ident" version = "1.0.25" @@ -7907,16 +8177,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" -[[package]] -name = "util_macros" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" -dependencies = [ - "perf", - "quote", - "syn 2.0.119", -] - [[package]] name = "uuid" version = "1.26.1" @@ -8120,7 +8380,8 @@ dependencies = [ [[package]] name = "wasm_thread" version = "0.3.3" -source = "git+https://github.com/zed-industries/wasm_thread?rev=0cf96c7708dfb97ccf3da50347e25edcf75d6937#0cf96c7708dfb97ccf3da50347e25edcf75d6937" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7516db7f32decdadb1c3b8deb1b7d78b9df7606c5cc2f6241737c2ab3a0258e" dependencies = [ "futures", "js-sys", @@ -9095,7 +9356,7 @@ dependencies = [ "chat_ui", "common", "device", - "gpui", + "gpui-pre", "instant", "log", "nostr-connect", @@ -9176,15 +9437,17 @@ checksum = "163b33ed8786455e2fa5d72f554057ce3f3182425434f756cd39c99839d88e23" [[package]] name = "xim-ctext" version = "0.3.0" -source = "git+https://github.com/zed-industries/xim-rs.git?rev=16f35a2c881b815a2b6cdfd6687988e84f8447d8#16f35a2c881b815a2b6cdfd6687988e84f8447d8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ac61a7062c40f3c37b6e82eeeef835d5cc7824b632a72784a89b3963c33284c" dependencies = [ "encoding_rs", ] [[package]] name = "xim-parser" -version = "0.2.1" -source = "git+https://github.com/zed-industries/xim-rs.git?rev=16f35a2c881b815a2b6cdfd6687988e84f8447d8#16f35a2c881b815a2b6cdfd6687988e84f8447d8" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dcee45f89572d5a65180af3a84e7ddb24f5ea690a6d3aa9de231281544dd7b7" dependencies = [ "bitflags 2.13.2", ] @@ -9213,6 +9476,17 @@ version = "0.8.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" +[[package]] +name = "xml5ever" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bbb26405d8e919bc1547a5aa9abc95cbfa438f04844f5fdd9dc7596b748bf69" +dependencies = [ + "log", + "mac", + "markup5ever", +] + [[package]] name = "xmlwriter" version = "0.1.0" @@ -9374,14 +9648,15 @@ dependencies = [ [[package]] name = "zed-font-kit" version = "0.14.1-zed" -source = "git+https://github.com/zed-industries/font-kit?rev=94b0f28166665e8fd2f53ff6d268a14955c82269#94b0f28166665e8fd2f53ff6d268a14955c82269" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3898e450f36f852edda72e3f985c34426042c4951790b23b107f93394f9bff5" dependencies = [ "bitflags 2.13.2", "byteorder", "core-foundation 0.10.1", "core-graphics 0.24.0", "core-text", - "dirs 6.0.0", + "dirs", "dwrote", "float-ord", "freetype-sys", @@ -9395,59 +9670,11 @@ dependencies = [ "yeslogic-fontconfig-sys", ] -[[package]] -name = "zed-reqwest" -version = "0.12.15-zed" -source = "git+https://github.com/zed-industries/reqwest.git?rev=33bc764aa15ff7b200bf7c93bd96e24878d53e14#33bc764aa15ff7b200bf7c93bd96e24878d53e14" -dependencies = [ - "base64 0.22.1", - "bytes", - "encoding_rs", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "ipnet", - "js-sys", - "log", - "mime", - "mime_guess", - "once_cell", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-native-certs", - "rustls-pemfile", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "system-configuration", - "tokio", - "tokio-rustls", - "tokio-socks", - "tokio-util", - "tower", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", - "windows-registry 0.4.0", -] - [[package]] name = "zed-scap" version = "0.0.8-zed" -source = "git+https://github.com/zed-industries/scap?rev=4afea48c3b002197176fb19cd0f9b180dd36eaac#4afea48c3b002197176fb19cd0f9b180dd36eaac" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b338d705ae33a43ca00287c11129303a7a0aa57b101b72a1c08c863f698ac8" dependencies = [ "anyhow", "cocoa 0.25.0", @@ -9468,7 +9695,8 @@ dependencies = [ [[package]] name = "zed-xim" version = "0.4.0-zed" -source = "git+https://github.com/zed-industries/xim-rs.git?rev=16f35a2c881b815a2b6cdfd6687988e84f8447d8#16f35a2c881b815a2b6cdfd6687988e84f8447d8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0b46ed118eba34d9ba53d94ddc0b665e0e06a2cf874cfa2dd5dec278148642" dependencies = [ "ahash", "hashbrown 0.14.5", @@ -9584,39 +9812,12 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" -[[package]] -name = "zlog" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" -dependencies = [ - "anyhow", - "chrono", - "collections", - "log", -] - [[package]] name = "zmij" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" -[[package]] -name = "ztracing" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" -dependencies = [ - "tracing", - "tracing-subscriber", - "zlog", - "ztracing_macro", -] - -[[package]] -name = "ztracing_macro" -version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#4b47ceb9d328035bde5ef99009e674d9fee07fb7" - [[package]] name = "zune-core" version = "0.5.3" diff --git a/Cargo.toml b/Cargo.toml index 391680ca..8ced0833 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,14 +9,21 @@ edition = "2024" publish = false [workspace.dependencies] -# GPUI -gpui = { git = "https://github.com/zed-industries/zed" } -gpui_platform = { git = "https://github.com/zed-industries/zed", features = ["font-kit", "x11", "wayland"] } -gpui_linux = { git = "https://github.com/zed-industries/zed" } -gpui_windows = { git = "https://github.com/zed-industries/zed" } -gpui_macos = { git = "https://github.com/zed-industries/zed" } -gpui_tokio = { git = "https://github.com/zed-industries/zed" } -reqwest_client = { git = "https://github.com/zed-industries/zed" } +# GPUI. The `gpui-pre` family is upstream zed's gpui republished unchanged, so these +# aliases keep every `use gpui::..` site as it is while moving off the zed git pin. +gpui = { package = "gpui-pre", version = "0.3.5" } +gpui_platform = { package = "gpui-pre-platform", version = "0.3.5", features = ["font-kit", "x11", "wayland"] } +gpui_linux = { package = "gpui-pre-linux", version = "0.3.5" } +gpui_windows = { package = "gpui-pre-windows", version = "0.3.5" } +gpui_macos = { package = "gpui-pre-macos", version = "0.3.5" } +gpui_web = { package = "gpui-pre-web", version = "0.3.5" } +gpui_util = { package = "gpui-pre-util", version = "0.3.5" } +sum_tree = { package = "gpui-pre-sum-tree", version = "0.3.5" } +reqwest_client = { package = "gpui-pre-reqwest-client", version = "0.3.5" } +gpui_tokio = { path = "crates/gpui_tokio" } + +# Unstyled behavior, state, and infrastructure from GPUI Kit +gpui-base = "0.6.1" # Nostr nostr-lmdb = { git = "https://github.com/rust-nostr/nostr" } diff --git a/crates/gpui_tokio/Cargo.toml b/crates/gpui_tokio/Cargo.toml new file mode 100644 index 00000000..34d943fa --- /dev/null +++ b/crates/gpui_tokio/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "gpui_tokio" +version = "0.1.0" +edition.workspace = true +publish.workspace = true + +[dependencies] +anyhow.workspace = true +gpui.workspace = true +gpui_util.workspace = true +tokio = { version = "1", features = ["rt", "rt-multi-thread"] } diff --git a/crates/gpui_tokio/src/lib.rs b/crates/gpui_tokio/src/lib.rs new file mode 100644 index 00000000..7c2fff3f --- /dev/null +++ b/crates/gpui_tokio/src/lib.rs @@ -0,0 +1,102 @@ +//! Vendored from zed's `crates/gpui_tokio` (Apache-2.0) because the `gpui-pre` family +//! does not republish it, and `nostr-sdk`'s reqwest client needs a Tokio runtime. + +use std::future::Future; + +use gpui::{App, AppContext, Global, ReadGlobal, Task}; +use gpui_util::defer; +pub use tokio::task::JoinError; + +/// Initializes the Tokio wrapper using a new Tokio runtime with 2 worker threads. +/// +/// If you need more threads (or access to the runtime outside of GPUI), you can create the runtime +/// yourself and pass a Handle to `init_from_handle`. +pub fn init(cx: &mut App) { + let runtime = tokio::runtime::Builder::new_multi_thread() + // Since we now have two executors, let's try to keep our footprint small + .worker_threads(2) + .enable_all() + .build() + .expect("Failed to initialize Tokio"); + + let handle = runtime.handle().clone(); + cx.set_global(GlobalTokio { + owned_runtime: Some(runtime), + handle, + }); +} + +/// Initializes the Tokio wrapper using a Tokio runtime handle. +pub fn init_from_handle(cx: &mut App, handle: tokio::runtime::Handle) { + cx.set_global(GlobalTokio { + owned_runtime: None, + handle, + }); +} + +struct GlobalTokio { + owned_runtime: Option, + handle: tokio::runtime::Handle, +} + +impl Global for GlobalTokio {} + +impl Drop for GlobalTokio { + fn drop(&mut self) { + if let Some(runtime) = self.owned_runtime.take() { + runtime.shutdown_background(); + } + } +} + +pub struct Tokio {} + +impl Tokio { + /// Spawns the given future on Tokio's thread pool, and returns it via a GPUI task + /// Note that the Tokio task will be cancelled if the GPUI task is dropped + pub fn spawn(cx: &C, f: Fut) -> Task> + where + C: AppContext, + Fut: Future + Send + 'static, + R: Send + 'static, + { + cx.read_global(|tokio: &GlobalTokio, cx| { + let join_handle = tokio.handle.spawn(f); + let abort_handle = join_handle.abort_handle(); + let cancel = defer(move || { + abort_handle.abort(); + }); + cx.background_spawn(async move { + let result = join_handle.await; + drop(cancel); + result + }) + }) + } + + /// Spawns the given future on Tokio's thread pool, and returns it via a GPUI task + /// Note that the Tokio task will be cancelled if the GPUI task is dropped + pub fn spawn_result(cx: &C, f: Fut) -> Task> + where + C: AppContext, + Fut: Future> + Send + 'static, + R: Send + 'static, + { + cx.read_global(|tokio: &GlobalTokio, cx| { + let join_handle = tokio.handle.spawn(f); + let abort_handle = join_handle.abort_handle(); + let cancel = defer(move || { + abort_handle.abort(); + }); + cx.background_spawn(async move { + let result = join_handle.await?; + drop(cancel); + result + }) + }) + } + + pub fn handle(cx: &App) -> tokio::runtime::Handle { + GlobalTokio::global(cx).handle.clone() + } +} diff --git a/crates/theme/Cargo.toml b/crates/theme/Cargo.toml index 80e07c12..314f8d0e 100644 --- a/crates/theme/Cargo.toml +++ b/crates/theme/Cargo.toml @@ -6,6 +6,7 @@ publish.workspace = true [dependencies] gpui.workspace = true +gpui-base.workspace = true anyhow.workspace = true log.workspace = true serde.workspace = true diff --git a/crates/theme/src/lib.rs b/crates/theme/src/lib.rs index 830901ac..778a996e 100644 --- a/crates/theme/src/lib.rs +++ b/crates/theme/src/lib.rs @@ -46,6 +46,64 @@ pub fn init(cx: &mut App) { Theme::sync_scrollbar_appearance(cx); } +/// Mirror the active coop theme into the `gpui-base` global theme. +/// +/// Base paints a few things from its own tokens -- the focus ring, the wash +/// behind selected text, scrollbars, and overlay backdrops -- so the two +/// globals have to agree or those details drift away from the palette. +/// +/// Only roles base can act on are projected. Radius, spacing, typography sizes, +/// shadows, and scrollbar geometry keep their base defaults: coop has a single +/// `radius`/`radius_lg`/`font_size` where base has six-point scales, so any +/// mapping would be invented rather than derived. Revisit when a base component +/// is actually rendered. +/// +/// This is a no-op before the coop theme global exists; [`Theme::change`] is the +/// authoritative hook that keeps the projection current. +pub fn sync_base(cx: &mut App) { + let Some(theme) = cx.try_global::() else { + return; + }; + + let appearance = if theme.mode.is_dark() { + gpui_base::ThemeAppearance::Dark + } else { + gpui_base::ThemeAppearance::Light + }; + let scrollbar_mode = match theme.scrollbar_mode { + ScrollbarMode::Scrolling => gpui_base::ScrollbarMode::Scrolling, + ScrollbarMode::Hover => gpui_base::ScrollbarMode::Hover, + ScrollbarMode::Always => gpui_base::ScrollbarMode::Always, + }; + let colors = theme.colors; + let font_family = theme.font_family.clone(); + + let base = gpui_base::Theme::global_mut(cx); + base.appearance = appearance; + base.scrollbar = base.scrollbar.clone().with_mode(scrollbar_mode); + base.tokens.typography.sans = font_family; + + let tokens = &mut base.tokens.colors; + tokens.background = colors.background; + tokens.foreground = colors.text; + tokens.surface = colors.surface_background; + tokens.surface_foreground = colors.text; + tokens.primary = colors.element_background; + tokens.primary_foreground = colors.element_foreground; + tokens.secondary = colors.secondary_background; + tokens.secondary_foreground = colors.secondary_foreground; + tokens.muted = colors.ghost_element_background_alt; + tokens.muted_foreground = colors.text_muted; + tokens.accent = colors.ghost_element_hover; + tokens.accent_foreground = colors.text; + tokens.destructive = colors.danger_background; + tokens.destructive_foreground = colors.danger_foreground; + tokens.border = colors.border; + tokens.input = colors.border; + tokens.ring = colors.ring; + tokens.selection = colors.selection; +} + pub trait ActiveTheme { fn theme(&self) -> &Theme; } @@ -183,6 +241,9 @@ impl Theme { if let Some(window) = window { window.refresh(); } + + // Keep the base-layer projection in step with the coop palette + sync_base(cx); } } diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index ce16ba98..2a612f33 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -9,6 +9,7 @@ common = { path = "../common" } theme = { path = "../theme" } gpui.workspace = true +gpui-base.workspace = true instant.workspace = true serde.workspace = true smallvec.workspace = true @@ -21,7 +22,7 @@ uuid = "1.10" regex = "1" lsp-types = "0.97.0" ropey = { version = "=2.0.0-beta.1", features = ["metric_lines_lf", "metric_utf16"] } -sum_tree = { git = "https://github.com/zed-industries/zed" } +sum_tree.workspace = true [target.'cfg(not(target_arch = "wasm32"))'.dependencies] smol.workspace = true diff --git a/crates/ui/src/checkbox.rs b/crates/ui/src/checkbox.rs deleted file mode 100644 index 1ce8e19e..00000000 --- a/crates/ui/src/checkbox.rs +++ /dev/null @@ -1,312 +0,0 @@ -use std::rc::Rc; -use instant::Duration; - -use gpui::prelude::FluentBuilder as _; -use gpui::{ - div, px, relative, rems, svg, Animation, AnimationExt, AnyElement, App, Div, ElementId, - InteractiveElement, IntoElement, ParentElement, RenderOnce, SharedString, - StatefulInteractiveElement, StyleRefinement, Styled, Window, -}; -use theme::ActiveTheme; - -use crate::icon::IconNamed; -use crate::{v_flex, Disableable, IconName, Selectable, Sizable, Size, StyledExt as _}; - -/// A Checkbox element. -#[allow(clippy::type_complexity)] -#[derive(IntoElement)] -pub struct Checkbox { - id: ElementId, - base: Div, - style: StyleRefinement, - label: Option, - children: Vec, - checked: bool, - disabled: bool, - size: Size, - tab_stop: bool, - tab_index: isize, - on_click: Option>, -} - -impl Checkbox { - /// Create a new Checkbox with the given id. - pub fn new(id: impl Into) -> Self { - Self { - id: id.into(), - base: div(), - style: StyleRefinement::default(), - label: None, - children: Vec::new(), - checked: false, - disabled: false, - size: Size::default(), - on_click: None, - tab_stop: true, - tab_index: 0, - } - } - - /// Set the label for the checkbox. - pub fn label(mut self, label: impl Into) -> Self { - self.label = Some(label.into()); - self - } - - /// Set the checked state for the checkbox. - pub fn checked(mut self, checked: bool) -> Self { - self.checked = checked; - self - } - - /// Set the click handler for the checkbox. - /// - /// The `&bool` parameter indicates the new checked state after the click. - pub fn on_click(mut self, handler: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self { - self.on_click = Some(Rc::new(handler)); - self - } - - /// Set the tab stop for the checkbox, default is true. - pub fn tab_stop(mut self, tab_stop: bool) -> Self { - self.tab_stop = tab_stop; - self - } - - /// Set the tab index for the checkbox, default is 0. - pub fn tab_index(mut self, tab_index: isize) -> Self { - self.tab_index = tab_index; - self - } - - #[allow(clippy::type_complexity)] - fn handle_click( - on_click: &Option>, - checked: bool, - window: &mut Window, - cx: &mut App, - ) { - let new_checked = !checked; - if let Some(f) = on_click { - (f)(&new_checked, window, cx); - } - } -} - -impl InteractiveElement for Checkbox { - fn interactivity(&mut self) -> &mut gpui::Interactivity { - self.base.interactivity() - } -} -impl StatefulInteractiveElement for Checkbox {} - -impl Styled for Checkbox { - fn style(&mut self) -> &mut gpui::StyleRefinement { - &mut self.style - } -} - -impl Disableable for Checkbox { - fn disabled(mut self, disabled: bool) -> Self { - self.disabled = disabled; - self - } -} - -impl Selectable for Checkbox { - fn selected(self, selected: bool) -> Self { - self.checked(selected) - } - - fn is_selected(&self) -> bool { - self.checked - } -} - -impl ParentElement for Checkbox { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements); - } -} - -impl Sizable for Checkbox { - fn with_size(mut self, size: impl Into) -> Self { - self.size = size.into(); - self - } -} - -pub(crate) fn checkbox_check_icon( - id: ElementId, - size: Size, - checked: bool, - disabled: bool, - window: &mut Window, - cx: &mut App, -) -> impl IntoElement { - let toggle_state = window.use_keyed_state(id, cx, |_, _| checked); - - let color = if disabled { - cx.theme().text.opacity(0.5) - } else { - cx.theme().text - }; - - svg() - .absolute() - .top_px() - .left_px() - .map(|this| match size { - Size::XSmall => this.size_2(), - Size::Small => this.size_2p5(), - Size::Medium => this.size_3(), - Size::Large => this.size_3p5(), - _ => this.size_3(), - }) - .text_color(color) - .map(|this| match checked { - true => this.path(IconName::Check.path()), - _ => this, - }) - .map(|this| { - if !disabled && checked != *toggle_state.read(cx) { - let duration = Duration::from_secs_f64(0.25); - cx.spawn({ - let toggle_state = toggle_state.clone(); - async move |cx| { - cx.background_executor().timer(duration).await; - toggle_state.update(cx, |this, _| *this = checked); - } - }) - .detach(); - - this.with_animation( - ElementId::NamedInteger("toggle".into(), checked as u64), - Animation::new(Duration::from_secs_f64(0.25)), - move |this, delta| { - this.opacity(if checked { 1.0 * delta } else { 1.0 - delta }) - }, - ) - .into_any_element() - } else { - this.into_any_element() - } - }) -} - -impl RenderOnce for Checkbox { - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let focus_handle = window - .use_keyed_state(self.id.clone(), cx, |_, cx| cx.focus_handle()) - .read(cx) - .clone(); - - let checked = self.checked; - let radius = cx.theme().radius.min(px(4.)); - - let border_color = if checked { - cx.theme().border_focused - } else { - cx.theme().border - }; - - let color = if self.disabled { - border_color.opacity(0.5) - } else { - border_color - }; - - div().child( - self.base - .id(self.id.clone()) - .when(!self.disabled, |this| { - this.track_focus( - &focus_handle - .tab_stop(self.tab_stop) - .tab_index(self.tab_index), - ) - }) - .h_flex() - .gap_2() - .items_start() - .line_height(relative(1.)) - .text_color(cx.theme().text) - .map(|this| match self.size { - Size::XSmall => this.text_xs(), - Size::Small => this.text_sm(), - Size::Medium => this.text_base(), - Size::Large => this.text_lg(), - _ => this, - }) - .when(self.disabled, |this| this.text_color(cx.theme().text_muted)) - .rounded(cx.theme().radius * 0.5) - .refine_style(&self.style) - .child( - div() - .relative() - .map(|this| match self.size { - Size::XSmall => this.size_3(), - Size::Small => this.size_3p5(), - Size::Medium => this.size_4(), - Size::Large => this.size(rems(1.125)), - _ => this.size_4(), - }) - .flex_shrink_0() - .border_1() - .border_color(color) - .rounded(radius) - .when(cx.theme().shadow && !self.disabled, |this| this.shadow_xs()) - .map(|this| match checked { - false => this.bg(cx.theme().background), - _ => this.bg(color), - }) - .child(checkbox_check_icon( - self.id, - self.size, - checked, - self.disabled, - window, - cx, - )), - ) - .when(self.label.is_some() || !self.children.is_empty(), |this| { - this.child( - v_flex() - .w_full() - .line_height(relative(1.2)) - .gap_1() - .map(|this| { - if let Some(label) = self.label { - this.child( - div() - .size_full() - .text_color(cx.theme().text) - .when(self.disabled, |this| { - this.text_color(cx.theme().text_muted) - }) - .line_height(relative(1.)) - .child(label), - ) - } else { - this - } - }) - .children(self.children), - ) - }) - .on_mouse_down(gpui::MouseButton::Left, |_, window, _| { - // Avoid focus on mouse down. - window.prevent_default(); - }) - .when(!self.disabled, |this| { - this.on_click({ - let on_click = self.on_click.clone(); - move |_, window, cx| { - window.prevent_default(); - Self::handle_click(&on_click, checked, window, cx); - } - }) - }), - ) - } -} diff --git a/crates/ui/src/element_ext.rs b/crates/ui/src/element_ext.rs deleted file mode 100644 index 90de5ec6..00000000 --- a/crates/ui/src/element_ext.rs +++ /dev/null @@ -1,27 +0,0 @@ -use gpui::{canvas, App, Bounds, ParentElement, Pixels, Styled as _, Window}; - -/// A trait to extend [`gpui::Element`] with additional functionality. -pub trait ElementExt: ParentElement + Sized { - /// Add a prepaint callback to the element. - /// - /// This is a helper method to get the bounds of the element after paint. - /// - /// The first argument is the bounds of the element in pixels. - /// - /// See also [`gpui::canvas`]. - fn on_prepaint(self, f: F) -> Self - where - F: FnOnce(Bounds, &mut Window, &mut App) + 'static, - { - self.child( - canvas( - move |bounds, window, cx| f(bounds, window, cx), - |_, _, _, _| {}, - ) - .absolute() - .size_full(), - ) - } -} - -impl ElementExt for T {} diff --git a/crates/ui/src/event.rs b/crates/ui/src/event.rs deleted file mode 100644 index 5cd2035c..00000000 --- a/crates/ui/src/event.rs +++ /dev/null @@ -1,21 +0,0 @@ -use gpui::{App, ClickEvent, InteractiveElement, Stateful, Window}; - -pub trait InteractiveElementExt: InteractiveElement { - /// Set the listener for a double click event. - fn on_double_click( - mut self, - listener: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, - ) -> Self - where - Self: Sized, - { - self.interactivity().on_click(move |event, window, cx| { - if event.click_count() == 2 { - listener(event, window, cx); - } - }); - self - } -} - -impl InteractiveElementExt for Stateful {} diff --git a/crates/ui/src/focusable.rs b/crates/ui/src/focusable.rs deleted file mode 100644 index 7ecd8439..00000000 --- a/crates/ui/src/focusable.rs +++ /dev/null @@ -1,39 +0,0 @@ -use gpui::{Context, FocusHandle, Window}; - -/// A trait for views that can cycle focus between its children. -/// -/// This will provide a default implementation for the `cycle_focus` method that will cycle focus. -/// -/// You should implement the `cycle_focus_handles` method to return a list of focus handles that -/// should be cycled, and the cycle will follow the order of the list. -pub trait FocusableCycle { - /// Returns a list of focus handles that should be cycled. - fn cycle_focus_handles(&self, window: &mut Window, cx: &mut Context) -> Vec - where - Self: Sized; - - /// Cycles focus between the focus handles returned by `cycle_focus_handles`. - /// If `is_next` is `true`, it will cycle to the next focus handle, otherwise it will cycle to prev. - fn cycle_focus(&self, is_next: bool, window: &mut Window, cx: &mut Context) - where - Self: Sized, - { - let focused_handle = window.focused(cx); - let handles = self.cycle_focus_handles(window, cx); - let handles = if is_next { - handles - } else { - handles.into_iter().rev().collect() - }; - - let fallback_handle = handles[0].clone(); - let target_focus_handle = handles - .into_iter() - .skip_while(|handle| Some(handle) != focused_handle.as_ref()) - .nth(1) - .unwrap_or(fallback_handle); - - target_focus_handle.focus(window, cx); - cx.stop_propagation(); - } -} diff --git a/crates/ui/src/index_path.rs b/crates/ui/src/index_path.rs deleted file mode 100644 index 987412eb..00000000 --- a/crates/ui/src/index_path.rs +++ /dev/null @@ -1,69 +0,0 @@ -use std::fmt::{Debug, Display}; - -use gpui::ElementId; - -/// Represents an index path in a list, which consists of a section index, -/// -/// The default values for section, row, and column are all set to 0. -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] -pub struct IndexPath { - /// The section index. - pub section: usize, - /// The item index in the section. - pub row: usize, - /// The column index. - pub column: usize, -} - -impl From for ElementId { - fn from(path: IndexPath) -> Self { - ElementId::Name(format!("index-path({},{},{})", path.section, path.row, path.column).into()) - } -} - -impl Display for IndexPath { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "IndexPath(section: {}, row: {}, column: {})", - self.section, self.row, self.column - ) - } -} - -impl IndexPath { - /// Create a new index path with the specified section and row. - /// - /// The `section` is set to 0 by default. - /// The `column` is set to 0 by default. - pub fn new(row: usize) -> Self { - IndexPath { - section: 0, - row, - ..Default::default() - } - } - - /// Set the section for the index path. - pub fn section(mut self, section: usize) -> Self { - self.section = section; - self - } - - /// Set the row for the index path. - pub fn row(mut self, row: usize) -> Self { - self.row = row; - self - } - - /// Set the column for the index path. - pub fn column(mut self, column: usize) -> Self { - self.column = column; - self - } - - /// Check if the self is equal to the given index path (Same section and row). - pub fn eq_row(&self, index: IndexPath) -> bool { - self.section == index.section && self.row == index.row - } -} diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index 2cadb085..114becd9 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -1,8 +1,5 @@ -pub use element_ext::ElementExt; -pub use event::InteractiveElementExt; -pub use focusable::FocusableCycle; +pub use gpui_base::{ElementExt, IndexPath, InteractiveElementExt}; pub use icon::*; -pub use index_path::IndexPath; pub use kbd::*; pub use root::{Root, window_paddings}; pub use styled::*; @@ -15,14 +12,12 @@ pub mod actions; pub mod animation; pub mod avatar; pub mod button; -pub mod checkbox; pub mod divider; pub mod dock; pub mod group_box; pub mod history; pub mod indicator; pub mod input; -pub mod list; pub mod menu; pub mod modal; pub mod notification; @@ -34,11 +29,7 @@ pub mod switch; pub mod tab; pub mod tooltip; -mod element_ext; -mod event; -mod focusable; mod icon; -mod index_path; mod kbd; mod root; mod styled; @@ -50,8 +41,9 @@ mod window_ext; /// This must be called before using any of the UI components. /// You can initialize the UI module at your application's entry point. pub fn init(cx: &mut gpui::App) { + gpui_base::init(cx); + theme::sync_base(cx); input::init(cx); - list::init(cx); modal::init(cx); popover::init(cx); menu::init(cx); diff --git a/crates/ui/src/list/cache.rs b/crates/ui/src/list/cache.rs deleted file mode 100644 index 3de7a8c1..00000000 --- a/crates/ui/src/list/cache.rs +++ /dev/null @@ -1,221 +0,0 @@ -use std::rc::Rc; - -use gpui::{App, Pixels, Size}; - -use crate::IndexPath; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum RowEntry { - Entry(IndexPath), - SectionHeader(usize), - SectionFooter(usize), -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub(crate) struct MeasuredEntrySize { - pub(crate) item_size: Size, - pub(crate) section_header_size: Size, - pub(crate) section_footer_size: Size, -} - -impl RowEntry { - #[inline] - #[allow(unused)] - pub(crate) fn is_section_header(&self) -> bool { - matches!(self, RowEntry::SectionHeader(_)) - } - - pub(crate) fn eq_index_path(&self, path: &IndexPath) -> bool { - match self { - RowEntry::Entry(index_path) => index_path == path, - RowEntry::SectionHeader(_) | RowEntry::SectionFooter(_) => false, - } - } - - #[allow(unused)] - pub(crate) fn index(&self) -> IndexPath { - match self { - RowEntry::Entry(index_path) => *index_path, - RowEntry::SectionHeader(ix) => IndexPath::default().section(*ix), - RowEntry::SectionFooter(ix) => IndexPath::default().section(*ix), - } - } - - #[inline] - #[allow(unused)] - pub(crate) fn is_section_footer(&self) -> bool { - matches!(self, RowEntry::SectionFooter(_)) - } - - #[inline] - pub(crate) fn is_entry(&self) -> bool { - matches!(self, RowEntry::Entry(_)) - } - - #[inline] - #[allow(unused)] - pub(crate) fn section_ix(&self) -> Option { - match self { - RowEntry::SectionHeader(ix) | RowEntry::SectionFooter(ix) => Some(*ix), - _ => None, - } - } -} - -#[derive(Default, Clone)] -pub(crate) struct RowsCache { - /// Only have section's that have rows. - pub(crate) entities: Rc>, - pub(crate) items_count: usize, - /// The sections, the item is number of rows in each section. - pub(crate) sections: Rc>, - pub(crate) entries_sizes: Rc>>, - measured_size: MeasuredEntrySize, -} - -impl RowsCache { - pub(crate) fn get(&self, flatten_ix: usize) -> Option { - self.entities.get(flatten_ix).cloned() - } - - /// Returns the number of flattened rows (Includes header, item, footer). - pub(crate) fn len(&self) -> usize { - self.entities.len() - } - - /// Return the number of items in the cache. - pub(crate) fn items_count(&self) -> usize { - self.items_count - } - - /// Returns the index of the Entry with given path in the flattened rows. - pub(crate) fn position_of(&self, path: &IndexPath) -> Option { - self.entities - .iter() - .position(|p| p.is_entry() && p.eq_index_path(path)) - } - - /// Return prev row, if the row is the first in the first section, goes to the last row. - /// - /// Empty rows section are skipped. - pub(crate) fn prev(&self, path: Option) -> IndexPath { - let path = path.unwrap_or_default(); - let Some(pos) = self.position_of(&path) else { - return self - .entities - .iter() - .rfind(|entry| entry.is_entry()) - .map(|entry| entry.index()) - .unwrap_or_default(); - }; - - if let Some(path) = self - .entities - .iter() - .take(pos) - .rev() - .find(|entry| entry.is_entry()) - .map(|entry| entry.index()) - { - path - } else { - self.entities - .iter() - .rfind(|entry| entry.is_entry()) - .map(|entry| entry.index()) - .unwrap_or_default() - } - } - - /// Returns the next row, if the row is the last in the last section, goes to the first row. - /// - /// Empty rows section are skipped. - pub(crate) fn next(&self, path: Option) -> IndexPath { - let Some(mut path) = path else { - return IndexPath::default(); - }; - - let Some(pos) = self.position_of(&path) else { - return self - .entities - .iter() - .find(|entry| entry.is_entry()) - .map(|entry| entry.index()) - .unwrap_or_default(); - }; - - if let Some(next_path) = self - .entities - .iter() - .skip(pos + 1) - .find(|entry| entry.is_entry()) - .map(|entry| entry.index()) - { - path = next_path; - } else { - path = self - .entities - .iter() - .find(|entry| entry.is_entry()) - .map(|entry| entry.index()) - .unwrap_or_default() - } - - path - } - - pub(crate) fn prepare_if_needed( - &mut self, - sections_count: usize, - measured_size: MeasuredEntrySize, - cx: &App, - rows_count_f: F, - ) where - F: Fn(usize, &App) -> usize, - { - let mut new_sections = vec![]; - for section_ix in 0..sections_count { - new_sections.push(rows_count_f(section_ix, cx)); - } - - let need_update = new_sections != *self.sections || self.measured_size != measured_size; - - if !need_update { - return; - } - - let mut entries_sizes = vec![]; - let mut total_items_count = 0; - self.measured_size = measured_size; - self.sections = Rc::new(new_sections); - self.entities = Rc::new( - self.sections - .iter() - .enumerate() - .flat_map(|(section, items_count)| { - total_items_count += items_count; - let mut children = vec![]; - if *items_count == 0 { - return children; - } - - children.push(RowEntry::SectionHeader(section)); - entries_sizes.push(measured_size.section_header_size); - for row in 0..*items_count { - children.push(RowEntry::Entry(IndexPath { - section, - row, - ..Default::default() - })); - entries_sizes.push(measured_size.item_size); - } - children.push(RowEntry::SectionFooter(section)); - entries_sizes.push(measured_size.section_footer_size); - children - }) - .collect(), - ); - self.entries_sizes = Rc::new(entries_sizes); - self.items_count = total_items_count; - } -} diff --git a/crates/ui/src/list/delegate.rs b/crates/ui/src/list/delegate.rs deleted file mode 100644 index 2899d2ff..00000000 --- a/crates/ui/src/list/delegate.rs +++ /dev/null @@ -1,171 +0,0 @@ -use gpui::{AnyElement, App, Context, IntoElement, ParentElement as _, Styled as _, Task, Window}; -use theme::ActiveTheme; - -use crate::list::loading::Loading; -use crate::list::ListState; -use crate::{h_flex, Icon, IconName, IndexPath, Selectable}; - -/// A delegate for the List. -#[allow(unused)] -pub trait ListDelegate: Sized + 'static { - type Item: Selectable + IntoElement; - - /// When Query Input change, this method will be called. - /// You can perform search here. - fn perform_search( - &mut self, - query: &str, - window: &mut Window, - cx: &mut Context>, - ) -> Task<()> { - Task::ready(()) - } - - /// Return the number of sections in the list, default is 1. - /// - /// Min value is 1. - fn sections_count(&self, cx: &App) -> usize { - 1 - } - - /// Return the number of items in the section at the given index. - /// - /// NOTE: Only the sections with items_count > 0 will be rendered. If the section has 0 items, - /// the section header and footer will also be skipped. - fn items_count(&self, section: usize, cx: &App) -> usize; - - /// Render the item at the given index. - /// - /// Return None will skip the item. - /// - /// NOTE: Every item should have same height. - fn render_item( - &mut self, - ix: IndexPath, - window: &mut Window, - cx: &mut Context>, - ) -> Option; - - /// Render the section header at the given index, default is None. - /// - /// NOTE: Every header should have same height. - fn render_section_header( - &mut self, - section: usize, - window: &mut Window, - cx: &mut Context>, - ) -> Option { - None:: - } - - /// Render the section footer at the given index, default is None. - /// - /// NOTE: Every footer should have same height. - fn render_section_footer( - &mut self, - section: usize, - window: &mut Window, - cx: &mut Context>, - ) -> Option { - None:: - } - - /// Return a Element to show when list is empty. - fn render_empty( - &mut self, - window: &mut Window, - cx: &mut Context>, - ) -> impl IntoElement { - h_flex() - .size_full() - .justify_center() - .text_color(cx.theme().text_muted.opacity(0.6)) - .child(Icon::new(IconName::Inbox).size_12()) - .into_any_element() - } - - /// Returns Some(AnyElement) to render the initial state of the list. - /// - /// This can be used to show a view for the list before the user has - /// interacted with it. - /// - /// For example: The last search results, or the last selected item. - /// - /// Default is None, that means no initial state. - fn render_initial( - &mut self, - window: &mut Window, - cx: &mut Context>, - ) -> Option { - None - } - - /// Returns the loading state to show the loading view. - fn loading(&self, cx: &App) -> bool { - false - } - - /// Returns a Element to show when loading, default is built-in Skeleton - /// loading view. - fn render_loading( - &mut self, - window: &mut Window, - cx: &mut Context>, - ) -> impl IntoElement { - Loading - } - - /// Set the selected index, just store the ix, don't confirm. - fn set_selected_index( - &mut self, - ix: Option, - window: &mut Window, - cx: &mut Context>, - ); - - /// Set the index of the item that has been right clicked. - fn set_right_clicked_index( - &mut self, - ix: Option, - window: &mut Window, - cx: &mut Context>, - ) { - } - - /// Set the confirm and give the selected index, - /// this is means user have clicked the item or pressed Enter. - /// - /// This will always to `set_selected_index` before confirm. - fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context>) { - } - - /// Cancel the selection, e.g.: Pressed ESC. - fn cancel(&mut self, window: &mut Window, cx: &mut Context>) {} - - /// Return true to enable load more data when scrolling to the bottom. - /// - /// Default: false - fn has_more(&self, cx: &App) -> bool { - false - } - - /// Returns a threshold value (n entities), of course, - /// when scrolling to the bottom, the remaining number of rows - /// triggers `load_more`. - /// - /// This should smaller than the total number of first load rows. - /// - /// Default: 20 entities (section header, footer and row) - fn load_more_threshold(&self) -> usize { - 20 - } - - /// Load more data when the table is scrolled to the bottom. - /// - /// This will performed in a background task. - /// - /// This is always called when the table is near the bottom, - /// so you must check if there is more data to load or lock - /// the loading state. - fn load_more(&mut self, window: &mut Window, cx: &mut Context>) {} -} diff --git a/crates/ui/src/list/list.rs b/crates/ui/src/list/list.rs deleted file mode 100644 index 8b2d4d67..00000000 --- a/crates/ui/src/list/list.rs +++ /dev/null @@ -1,747 +0,0 @@ -use std::ops::Range; - -use gpui::prelude::FluentBuilder; -use gpui::{ - App, AppContext, AvailableSpace, ClickEvent, Context, DefiniteLength, EdgesRefinement, Entity, - EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, KeyBinding, Length, - ListSizingBehavior, MouseButton, ParentElement, Render, RenderOnce, ScrollStrategy, - SharedString, StatefulInteractiveElement, StyleRefinement, Styled, Subscription, Task, - UniformListScrollHandle, Window, div, px, size, uniform_list, -}; -use instant::Duration; -use theme::ActiveTheme; - -use crate::actions::{Cancel, Confirm, SelectDown, SelectUp}; -use crate::input::{Input, InputEvent, InputState}; -use crate::list::ListDelegate; -use crate::list::cache::{MeasuredEntrySize, RowEntry, RowsCache}; -use crate::scroll::{Scrollbar, ScrollbarHandle}; -use crate::{Icon, IconName, IndexPath, Selectable, Sizable, Size, StyledExt, v_flex}; - -pub(crate) fn init(cx: &mut App) { - let context: Option<&str> = Some("List"); - cx.bind_keys([ - KeyBinding::new("escape", Cancel, context), - KeyBinding::new("enter", Confirm { secondary: false }, context), - KeyBinding::new("secondary-enter", Confirm { secondary: true }, context), - KeyBinding::new("up", SelectUp, context), - KeyBinding::new("down", SelectDown, context), - ]); -} - -#[derive(Clone)] -pub enum ListEvent { - /// Move to select item. - Select(IndexPath), - /// Click on item or pressed Enter. - Confirm(IndexPath), - /// Pressed ESC to deselect the item. - Cancel, -} - -struct ListOptions { - size: Size, - scrollbar_visible: bool, - search_placeholder: Option, - max_height: Option, - paddings: EdgesRefinement, -} - -impl Default for ListOptions { - fn default() -> Self { - Self { - size: Size::default(), - scrollbar_visible: true, - max_height: None, - search_placeholder: None, - paddings: EdgesRefinement::default(), - } - } -} - -/// The state for List. -/// -/// List required all items has the same height. -pub struct ListState { - pub(crate) focus_handle: FocusHandle, - pub(crate) query_input: Entity, - options: ListOptions, - delegate: D, - last_query: Option, - scroll_handle: UniformListScrollHandle, - rows_cache: RowsCache, - selected_index: Option, - item_to_measure_index: IndexPath, - deferred_scroll_to_index: Option<(IndexPath, ScrollStrategy)>, - mouse_right_clicked_index: Option, - reset_on_cancel: bool, - searchable: bool, - selectable: bool, - _search_task: Task<()>, - _load_more_task: Task<()>, - _query_input_subscription: Subscription, -} - -impl ListState -where - D: ListDelegate, -{ - pub fn new(delegate: D, window: &mut Window, cx: &mut Context) -> Self { - let query_input = cx.new(|cx| InputState::new(window, cx).placeholder("Search...")); - let _query_input_subscription = - cx.subscribe_in(&query_input, window, Self::on_query_input_event); - - Self { - focus_handle: cx.focus_handle(), - options: ListOptions::default(), - delegate, - rows_cache: RowsCache::default(), - query_input, - last_query: None, - selected_index: None, - selectable: true, - searchable: false, - item_to_measure_index: IndexPath::default(), - deferred_scroll_to_index: None, - mouse_right_clicked_index: None, - scroll_handle: UniformListScrollHandle::new(), - reset_on_cancel: true, - _search_task: Task::ready(()), - _load_more_task: Task::ready(()), - _query_input_subscription, - } - } - - /// Sets whether the list is searchable, default is `false`. - /// - /// When `true`, there will be a search input at the top of the list. - pub fn searchable(mut self, searchable: bool) -> Self { - self.searchable = searchable; - self - } - - pub fn set_searchable(&mut self, searchable: bool, cx: &mut Context) { - self.searchable = searchable; - cx.notify(); - } - - /// Sets whether the list is selectable, default is true. - pub fn selectable(mut self, selectable: bool) -> Self { - self.selectable = selectable; - self - } - - /// Sets whether the list is selectable, default is true. - pub fn set_selectable(&mut self, selectable: bool, cx: &mut Context) { - self.selectable = selectable; - cx.notify(); - } - - pub fn delegate(&self) -> &D { - &self.delegate - } - - pub fn delegate_mut(&mut self) -> &mut D { - &mut self.delegate - } - - /// Focus the list, if the list is searchable, focus the search input. - pub fn focus(&mut self, window: &mut Window, cx: &mut App) { - self.focus_handle(cx).focus(window, cx); - } - - /// Return true if either the list or the search input is focused. - #[allow(dead_code)] - pub(crate) fn is_focused(&self, window: &Window, cx: &App) -> bool { - self.focus_handle.is_focused(window) || self.query_input.focus_handle(cx).is_focused(window) - } - - /// Set the selected index of the list, - /// this will also scroll to the selected item. - pub(crate) fn _set_selected_index( - &mut self, - ix: Option, - window: &mut Window, - cx: &mut Context, - ) { - if !self.selectable { - return; - } - - self.selected_index = ix; - self.delegate.set_selected_index(ix, window, cx); - self.scroll_to_selected_item(window, cx); - } - - /// Set the selected index of the list, - /// this method will not scroll to the selected item. - pub fn set_selected_index( - &mut self, - ix: Option, - window: &mut Window, - cx: &mut Context, - ) { - self.selected_index = ix; - self.delegate.set_selected_index(ix, window, cx); - } - - pub fn selected_index(&self) -> Option { - self.selected_index - } - - /// Set the index of the item that has been right clicked. - pub fn set_right_clicked_index( - &mut self, - ix: Option, - window: &mut Window, - cx: &mut Context, - ) { - self.mouse_right_clicked_index = ix; - self.delegate.set_right_clicked_index(ix, window, cx); - } - - /// Returns the index of the item that has been right clicked. - pub fn right_clicked_index(&self) -> Option { - self.mouse_right_clicked_index - } - - /// Set a specific list item for measurement. - pub fn set_item_to_measure_index( - &mut self, - ix: IndexPath, - _: &mut Window, - cx: &mut Context, - ) { - self.item_to_measure_index = ix; - cx.notify(); - } - - /// Scroll to the item at the given index. - pub fn scroll_to_item( - &mut self, - ix: IndexPath, - strategy: ScrollStrategy, - _: &mut Window, - cx: &mut Context, - ) { - if ix.section == 0 && ix.row == 0 { - // If the item is the first item, scroll to the top. - let mut offset = self.scroll_handle.offset(); - offset.y = px(0.); - self.scroll_handle.set_offset(offset); - cx.notify(); - return; - } - self.deferred_scroll_to_index = Some((ix, strategy)); - cx.notify(); - } - - /// Get scroll handle - pub fn scroll_handle(&self) -> &UniformListScrollHandle { - &self.scroll_handle - } - - pub fn scroll_to_selected_item(&mut self, _: &mut Window, cx: &mut Context) { - if let Some(ix) = self.selected_index { - self.deferred_scroll_to_index = Some((ix, ScrollStrategy::Top)); - cx.notify(); - } - } - - fn on_query_input_event( - &mut self, - state: &Entity, - event: &InputEvent, - window: &mut Window, - cx: &mut Context, - ) { - match event { - InputEvent::Change => { - let text = state.read(cx).value(); - let text = text.trim().to_string(); - if Some(&text) == self.last_query.as_ref() { - return; - } - - self.set_searching(true, window, cx); - - let search = self.delegate.perform_search(&text, window, cx); - - if self.rows_cache.len() > 0 { - self._set_selected_index(Some(IndexPath::default()), window, cx); - } else { - self._set_selected_index(None, window, cx); - } - - let executor = cx.background_executor().clone(); - self._search_task = cx.spawn_in(window, async move |this, window| { - search.await; - - _ = this.update_in(window, |this, _, _| { - this.scroll_handle.scroll_to_item(0, ScrollStrategy::Top); - this.last_query = Some(text); - }); - - // Always wait 100ms to avoid flicker - executor.timer(Duration::from_millis(100)).await; - - _ = this.update_in(window, |this, window, cx| { - this.set_searching(false, window, cx); - }); - }); - } - InputEvent::PressEnter { secondary, .. } => self.on_action_confirm( - &Confirm { - secondary: *secondary, - }, - window, - cx, - ), - _ => {} - } - } - - fn set_searching(&mut self, searching: bool, _window: &mut Window, cx: &mut Context) { - self.query_input - .update(cx, |input, cx| input.set_loading(searching, cx)); - } - - /// Dispatch delegate's `load_more` method when the - /// visible range is near the end. - fn load_more_if_need( - &mut self, - entities_count: usize, - visible_end: usize, - window: &mut Window, - cx: &mut Context, - ) { - // FIXME: Here need void sections items count. - - let threshold = self.delegate.load_more_threshold(); - // Securely handle subtract logic to prevent attempt - // to subtract with overflow - if visible_end >= entities_count.saturating_sub(threshold) { - if !self.delegate.has_more(cx) { - return; - } - - self._load_more_task = cx.spawn_in(window, async move |view, cx| { - _ = view.update_in(cx, |view, window, cx| { - view.delegate.load_more(window, cx); - }); - }); - } - } - - #[allow(dead_code)] - pub(crate) fn reset_on_cancel(mut self, reset: bool) -> Self { - self.reset_on_cancel = reset; - self - } - - fn on_action_cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context) { - cx.propagate(); - if self.reset_on_cancel { - self._set_selected_index(None, window, cx); - } - - self.delegate.cancel(window, cx); - cx.emit(ListEvent::Cancel); - cx.notify(); - } - - fn on_action_confirm( - &mut self, - confirm: &Confirm, - window: &mut Window, - cx: &mut Context, - ) { - if self.rows_cache.len() == 0 { - return; - } - - let Some(ix) = self.selected_index else { - return; - }; - - self.delegate - .set_selected_index(self.selected_index, window, cx); - self.delegate.confirm(confirm.secondary, window, cx); - cx.emit(ListEvent::Confirm(ix)); - cx.notify(); - } - - fn select_item(&mut self, ix: IndexPath, window: &mut Window, cx: &mut Context) { - if !self.selectable { - return; - } - - self.selected_index = Some(ix); - self.delegate.set_selected_index(Some(ix), window, cx); - self.scroll_to_selected_item(window, cx); - cx.emit(ListEvent::Select(ix)); - cx.notify(); - } - - pub(crate) fn on_action_select_prev( - &mut self, - _: &SelectUp, - window: &mut Window, - cx: &mut Context, - ) { - if self.rows_cache.len() == 0 { - return; - } - - let prev_ix = self.rows_cache.prev(self.selected_index); - self.select_item(prev_ix, window, cx); - } - - pub(crate) fn on_action_select_next( - &mut self, - _: &SelectDown, - window: &mut Window, - cx: &mut Context, - ) { - if self.rows_cache.len() == 0 { - return; - } - - let next_ix = self.rows_cache.next(self.selected_index); - self.select_item(next_ix, window, cx); - } - - fn prepare_items_if_needed(&mut self, window: &mut Window, cx: &mut Context) { - let sections_count = self.delegate.sections_count(cx).max(1); - let mut measured_size = MeasuredEntrySize::default(); - - // Measure the item_height and section header/footer height. - let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent); - measured_size.item_size = self - .render_list_item(self.item_to_measure_index, window, cx) - .into_any_element() - .layout_as_root(available_space, window, cx); - - if let Some(mut el) = self - .delegate - .render_section_header(0, window, cx) - .map(|r| r.into_any_element()) - { - measured_size.section_header_size = el.layout_as_root(available_space, window, cx); - } - if let Some(mut el) = self - .delegate - .render_section_footer(0, window, cx) - .map(|r| r.into_any_element()) - { - measured_size.section_footer_size = el.layout_as_root(available_space, window, cx); - } - - self.rows_cache - .prepare_if_needed(sections_count, measured_size, cx, |section_ix, cx| { - self.delegate.items_count(section_ix, cx) - }); - } - - fn render_list_item( - &mut self, - ix: IndexPath, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let selectable = self.selectable; - let selected = self.selected_index.map(|s| s.eq_row(ix)).unwrap_or(false); - let mouse_right_clicked = self - .mouse_right_clicked_index - .map(|s| s.eq_row(ix)) - .unwrap_or(false); - let id = SharedString::from(format!("list-item-{}", ix)); - - div() - .id(id) - .w_full() - .relative() - .overflow_hidden() - .children(self.delegate.render_item(ix, window, cx).map(|item| { - item.selected(selected) - .secondary_selected(mouse_right_clicked) - })) - .when(selectable, |this| { - this.on_click(cx.listener(move |this, e: &ClickEvent, window, cx| { - this.set_right_clicked_index(None, window, cx); - this.selected_index = Some(ix); - this.on_action_confirm( - &Confirm { - secondary: e.modifiers().secondary(), - }, - window, - cx, - ); - })) - .on_mouse_down( - MouseButton::Right, - cx.listener(move |this, _, window, cx| { - this.set_right_clicked_index(Some(ix), window, cx); - cx.notify(); - }), - ) - }) - } - - fn render_items( - &mut self, - items_count: usize, - entities_count: usize, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let rows_cache = self.rows_cache.clone(); - let scrollbar_visible = self.options.scrollbar_visible; - let scroll_handle = self.scroll_handle.clone(); - - v_flex() - .flex_grow_1() - .relative() - .size_full() - .when_some(self.options.max_height, |this, h| this.max_h(h)) - .overflow_hidden() - .when(items_count == 0, |this| { - this.child(self.delegate.render_empty(window, cx)) - }) - .when(items_count > 0, { - |this| { - this.child( - uniform_list( - "virtual-list", - rows_cache.items_count(), - cx.processor(move |this, range: Range, window, cx| { - this.load_more_if_need(entities_count, range.end, window, cx); - - // NOTE: Here the v_virtual_list would not able to have gap_y, - // because the section header, footer is always have rendered as a empty child item, - // even the delegate give a None result. - - range - .map(|ix| { - let Some(entry) = rows_cache.get(ix) else { - return div(); - }; - - div().children(match entry { - RowEntry::Entry(index) => Some( - this.render_list_item(index, window, cx) - .into_any_element(), - ), - RowEntry::SectionHeader(section_ix) => this - .delegate_mut() - .render_section_header(section_ix, window, cx) - .map(|r| r.into_any_element()), - RowEntry::SectionFooter(section_ix) => this - .delegate_mut() - .render_section_footer(section_ix, window, cx) - .map(|r| r.into_any_element()), - }) - }) - .collect::>() - }), - ) - .when(self.options.max_height.is_some(), |this| { - this.with_sizing_behavior(ListSizingBehavior::Infer) - }) - .track_scroll(&scroll_handle) - .into_any_element(), - ) - } - }) - .when(scrollbar_visible, |this| { - this.child(Scrollbar::vertical(&scroll_handle)) - }) - } -} - -impl Focusable for ListState -where - D: ListDelegate, -{ - fn focus_handle(&self, cx: &App) -> FocusHandle { - if self.searchable { - self.query_input.focus_handle(cx) - } else { - self.focus_handle.clone() - } - } -} -impl EventEmitter for ListState where D: ListDelegate {} -impl Render for ListState -where - D: ListDelegate, -{ - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - self.prepare_items_if_needed(window, cx); - - // Scroll to the selected item if it is set. - if let Some((ix, strategy)) = self.deferred_scroll_to_index.take() - && let Some(item_ix) = self.rows_cache.position_of(&ix) - { - self.scroll_handle.scroll_to_item(item_ix, strategy); - } - - let loading = self.delegate().loading(cx); - let query_input = if self.searchable { - // sync placeholder - if let Some(placeholder) = &self.options.search_placeholder { - self.query_input.update(cx, |input, cx| { - input.set_placeholder(placeholder.clone(), window, cx); - }); - } - Some(self.query_input.clone()) - } else { - None - }; - - let loading_view = if loading { - Some(self.delegate.render_loading(window, cx).into_any_element()) - } else { - None - }; - let initial_view = if let Some(input) = &query_input { - if input.read(cx).value().is_empty() { - self.delegate.render_initial(window, cx) - } else { - None - } - } else { - None - }; - let items_count = self.rows_cache.items_count(); - let entities_count = self.rows_cache.len(); - let mouse_right_clicked_index = self.mouse_right_clicked_index; - - v_flex() - .key_context("List") - .id("list-state") - .track_focus(&self.focus_handle) - .size_full() - .relative() - .overflow_hidden() - .when_some(query_input, |this, input| { - this.child( - div() - .map(|this| match self.options.size { - Size::Small => this.px_1p5(), - _ => this.px_2(), - }) - .border_b_1() - .border_color(cx.theme().border) - .child( - Input::new(&input) - .with_size(self.options.size) - .appearance(false) - .cleanable(true) - .p_0() - .prefix( - Icon::new(IconName::Search).text_color(cx.theme().text_muted), - ), - ), - ) - }) - .when(!loading, |this| { - this.on_action(cx.listener(Self::on_action_cancel)) - .on_action(cx.listener(Self::on_action_confirm)) - .on_action(cx.listener(Self::on_action_select_next)) - .on_action(cx.listener(Self::on_action_select_prev)) - .map(|this| { - if let Some(view) = initial_view { - this.child(view) - } else { - this.child(self.render_items(items_count, entities_count, window, cx)) - } - }) - // Click out to cancel right clicked row - .when(mouse_right_clicked_index.is_some(), |this| { - this.on_mouse_down_out(cx.listener(|this, _, window, cx| { - this.set_right_clicked_index(None, window, cx); - cx.notify(); - })) - }) - }) - .children(loading_view) - } -} - -/// The List element. -#[derive(IntoElement)] -pub struct List { - state: Entity>, - style: StyleRefinement, - options: ListOptions, -} - -impl List -where - D: ListDelegate + 'static, -{ - /// Create a new List element with the given ListState entity. - pub fn new(state: &Entity>) -> Self { - Self { - state: state.clone(), - style: StyleRefinement::default(), - options: ListOptions::default(), - } - } - - /// Set whether the scrollbar is visible, default is `true`. - pub fn scrollbar_visible(mut self, visible: bool) -> Self { - self.options.scrollbar_visible = visible; - self - } - - /// Sets the placeholder text for the search input. - pub fn search_placeholder(mut self, placeholder: impl Into) -> Self { - self.options.search_placeholder = Some(placeholder.into()); - self - } -} - -impl Styled for List -where - D: ListDelegate + 'static, -{ - fn style(&mut self) -> &mut StyleRefinement { - &mut self.style - } -} - -impl Sizable for List -where - D: ListDelegate + 'static, -{ - fn with_size(mut self, size: impl Into) -> Self { - self.options.size = size.into(); - self - } -} - -impl RenderOnce for List -where - D: ListDelegate + 'static, -{ - fn render(mut self, _: &mut Window, cx: &mut App) -> impl IntoElement { - // Take paddings, max_height to options, and clear them from style, - // because they would be applied to the inner virtual list. - self.options.paddings = self.style.padding.clone(); - self.options.max_height = self.style.max_size.height; - self.style.padding = EdgesRefinement::default(); - self.style.max_size.height = None; - - self.state.update(cx, |state, _| { - state.options = self.options; - }); - - div() - .id("list") - .size_full() - .refine_style(&self.style) - .child(self.state.clone()) - } -} diff --git a/crates/ui/src/list/list_item.rs b/crates/ui/src/list/list_item.rs deleted file mode 100644 index d2d872a7..00000000 --- a/crates/ui/src/list/list_item.rs +++ /dev/null @@ -1,226 +0,0 @@ -use gpui::prelude::FluentBuilder as _; -use gpui::{ - div, AnyElement, App, ClickEvent, Div, ElementId, InteractiveElement, IntoElement, - MouseMoveEvent, ParentElement, RenderOnce, Stateful, StatefulInteractiveElement as _, - StyleRefinement, Styled, Window, -}; -use smallvec::SmallVec; -use theme::ActiveTheme; - -use crate::{h_flex, Disableable, Icon, Selectable, Sizable as _, StyledExt}; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -enum ListItemMode { - #[default] - Entry, - Separator, -} - -impl ListItemMode { - #[inline] - fn is_separator(&self) -> bool { - matches!(self, ListItemMode::Separator) - } -} - -#[derive(IntoElement)] -pub struct ListItem { - base: Stateful
, - mode: ListItemMode, - style: StyleRefinement, - disabled: bool, - selected: bool, - secondary_selected: bool, - confirmed: bool, - check_icon: Option, - #[allow(clippy::type_complexity)] - on_click: Option>, - #[allow(clippy::type_complexity)] - on_mouse_enter: Option>, - #[allow(clippy::type_complexity)] - suffix: Option AnyElement + 'static>>, - children: SmallVec<[AnyElement; 2]>, -} - -impl ListItem { - pub fn new(id: impl Into) -> Self { - let id: ElementId = id.into(); - Self { - mode: ListItemMode::Entry, - base: h_flex().id(id), - style: StyleRefinement::default(), - disabled: false, - selected: false, - secondary_selected: false, - confirmed: false, - on_click: None, - on_mouse_enter: None, - check_icon: None, - suffix: None, - children: SmallVec::new(), - } - } - - /// Set this list item to as a separator, it not able to be selected. - pub fn separator(mut self) -> Self { - self.mode = ListItemMode::Separator; - self - } - - /// Set to show check icon, default is None. - pub fn check_icon(mut self, icon: impl Into) -> Self { - self.check_icon = Some(icon.into()); - self - } - - /// Set ListItem as the selected item style. - pub fn selected(mut self, selected: bool) -> Self { - self.selected = selected; - self - } - - /// Set ListItem as the confirmed item style, it will show a check icon. - pub fn confirmed(mut self, confirmed: bool) -> Self { - self.confirmed = confirmed; - self - } - - pub fn disabled(mut self, disabled: bool) -> Self { - self.disabled = disabled; - self - } - - /// Set the suffix element of the input field, for example a clear button. - pub fn suffix(mut self, builder: F) -> Self - where - F: Fn(&mut Window, &mut App) -> E + 'static, - E: IntoElement, - { - self.suffix = Some(Box::new(move |window, cx| { - builder(window, cx).into_any_element() - })); - self - } - - pub fn on_click( - mut self, - handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.on_click = Some(Box::new(handler)); - self - } - - pub fn on_mouse_enter( - mut self, - handler: impl Fn(&MouseMoveEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.on_mouse_enter = Some(Box::new(handler)); - self - } -} - -impl Disableable for ListItem { - fn disabled(mut self, disabled: bool) -> Self { - self.disabled = disabled; - self - } -} - -impl Selectable for ListItem { - fn selected(mut self, selected: bool) -> Self { - self.selected = selected; - self - } - - fn is_selected(&self) -> bool { - self.selected - } - - fn secondary_selected(mut self, selected: bool) -> Self { - self.secondary_selected = selected; - self - } -} - -impl Styled for ListItem { - fn style(&mut self) -> &mut gpui::StyleRefinement { - &mut self.style - } -} - -impl ParentElement for ListItem { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements); - } -} - -impl RenderOnce for ListItem { - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let is_active = self.confirmed || self.selected; - - let corner_radii = self.style.corner_radii.clone(); - - let _selected_style = StyleRefinement { - corner_radii, - ..Default::default() - }; - - let is_selectable = !(self.disabled || self.mode.is_separator()); - - self.base - .relative() - .gap_x_1() - .py_1() - .px_3() - .text_base() - .text_color(cx.theme().text) - .relative() - .items_center() - .justify_between() - .refine_style(&self.style) - .when(is_selectable, |this| { - this.when_some(self.on_click, |this, on_click| this.on_click(on_click)) - .when_some(self.on_mouse_enter, |this, on_mouse_enter| { - this.on_mouse_move(move |ev, window, cx| (on_mouse_enter)(ev, window, cx)) - }) - .when(!is_active, |this| { - this.hover(|this| this.bg(cx.theme().ghost_element_hover)) - }) - }) - .when(!is_selectable, |this| { - this.text_color(cx.theme().text_muted) - }) - .child( - h_flex() - .w_full() - .items_center() - .justify_between() - .gap_x_1() - .child(div().w_full().children(self.children)) - .when_some(self.check_icon, |this, icon| { - this.child( - div() - .w_5() - .items_center() - .justify_center() - .when(self.confirmed, |this| { - this.child(icon.small().text_color(cx.theme().text_muted)) - }), - ) - }), - ) - .when_some(self.suffix, |this, suffix| this.child(suffix(window, cx))) - .map(|this| { - if is_selectable && (self.selected || self.secondary_selected) { - let bg = if self.selected { - cx.theme().ghost_element_active - } else { - cx.theme().ghost_element_background - }; - this.bg(bg) - } else { - this - } - }) - } -} diff --git a/crates/ui/src/list/loading.rs b/crates/ui/src/list/loading.rs deleted file mode 100644 index 9ad64d02..00000000 --- a/crates/ui/src/list/loading.rs +++ /dev/null @@ -1,34 +0,0 @@ -use gpui::{IntoElement, ParentElement as _, RenderOnce, Styled}; - -use super::ListItem; -use crate::skeleton::Skeleton; -use crate::v_flex; - -#[derive(IntoElement)] -pub struct Loading; - -#[derive(IntoElement)] -struct LoadingItem; - -impl RenderOnce for LoadingItem { - fn render(self, _window: &mut gpui::Window, _cx: &mut gpui::App) -> impl IntoElement { - ListItem::new("skeleton").disabled(true).child( - v_flex() - .gap_1p5() - .overflow_hidden() - .child(Skeleton::new().h_5().w_48().max_w_full()) - .child(Skeleton::new().secondary().h_3().w_64().max_w_full()), - ) - } -} - -impl RenderOnce for Loading { - fn render(self, _window: &mut gpui::Window, _cx: &mut gpui::App) -> impl IntoElement { - v_flex() - .py_2p5() - .gap_3() - .child(LoadingItem) - .child(LoadingItem) - .child(LoadingItem) - } -} diff --git a/crates/ui/src/list/mod.rs b/crates/ui/src/list/mod.rs deleted file mode 100644 index 11105c10..00000000 --- a/crates/ui/src/list/mod.rs +++ /dev/null @@ -1,28 +0,0 @@ -pub(crate) mod cache; -mod delegate; -#[allow(clippy::module_inception)] -mod list; -mod list_item; -mod loading; -mod separator_item; - -pub use delegate::*; -pub use list::*; -pub use list_item::*; -pub use separator_item::*; -use serde::{Deserialize, Serialize}; - -/// Settings for List. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ListSettings { - /// Whether to use active highlight style on ListItem, default - pub active_highlight: bool, -} - -impl Default for ListSettings { - fn default() -> Self { - Self { - active_highlight: true, - } - } -} diff --git a/crates/ui/src/list/separator_item.rs b/crates/ui/src/list/separator_item.rs deleted file mode 100644 index b419a4e0..00000000 --- a/crates/ui/src/list/separator_item.rs +++ /dev/null @@ -1,50 +0,0 @@ -use gpui::{AnyElement, ParentElement, RenderOnce, StyleRefinement}; -use smallvec::SmallVec; - -use crate::list::ListItem; -use crate::{Selectable, StyledExt}; - -pub struct ListSeparatorItem { - style: StyleRefinement, - children: SmallVec<[AnyElement; 2]>, -} - -impl ListSeparatorItem { - pub fn new() -> Self { - Self { - style: StyleRefinement::default(), - children: SmallVec::new(), - } - } -} - -impl Default for ListSeparatorItem { - fn default() -> Self { - Self::new() - } -} - -impl ParentElement for ListSeparatorItem { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements); - } -} - -impl Selectable for ListSeparatorItem { - fn selected(self, _: bool) -> Self { - self - } - - fn is_selected(&self) -> bool { - false - } -} - -impl RenderOnce for ListSeparatorItem { - fn render(self, _: &mut gpui::Window, _: &mut gpui::App) -> impl gpui::IntoElement { - ListItem::new("separator") - .refine_style(&self.style) - .children(self.children) - .disabled(true) - } -} diff --git a/crates/ui/src/styled.rs b/crates/ui/src/styled.rs index 92c42469..da7482b4 100644 --- a/crates/ui/src/styled.rs +++ b/crates/ui/src/styled.rs @@ -1,4 +1,5 @@ use gpui::{App, DefiniteLength, Div, Edges, Pixels, Refineable, StyleRefinement, Styled, div, px}; +pub use gpui_base::component_traits::{Collapsible, Disableable, Selectable}; use serde::{Deserialize, Serialize}; use theme::ActiveTheme; @@ -110,26 +111,6 @@ impl From for Size { } } -/// A trait for defining element that can be selected. -pub trait Selectable: Sized { - /// Set the selected state of the element. - fn selected(self, selected: bool) -> Self; - - /// Returns true if the element is selected. - fn is_selected(&self) -> bool; - - /// Set is the element mouse right clicked, default do nothing. - fn secondary_selected(self, _: bool) -> Self { - self - } -} - -/// A trait for defining element that can be disabled. -pub trait Disableable { - /// Set the disabled state of the element. - fn disabled(self, disabled: bool) -> Self; -} - /// A trait for setting the size of an element. pub trait Sizable: Sized { /// Set the ui::Size of this element. @@ -267,9 +248,3 @@ impl StyleSized for T { } } } - -/// A trait for defining element that can be collapsed. -pub trait Collapsible { - fn collapsed(self, collapsed: bool) -> Self; - fn is_collapsed(&self) -> bool; -} diff --git a/docs/gpui-base-migration.md b/docs/gpui-base-migration.md index 09bca188..781572d1 100644 --- a/docs/gpui-base-migration.md +++ b/docs/gpui-base-migration.md @@ -9,15 +9,28 @@ The facts below were checked against `gpui-base 0.6.1` (crates.io), the `gpui-ki repository at `main`, and this workspace's `Cargo.lock` (zed at `4b47ceb`, 2026-09-17). Line counts come from `wc -l` under `crates/ui/src`. +## Status + +- **Phase 0: landed.** Manifest only; no Rust changed. The API drift across the + three days between the snapshot and the old pin turned out to be purely + additive, so nothing had to be fixed. +- **Phase 1: landed.** Base is wired in, `sync_base` is in place, and 1,945 lines + of dead weight are gone. `history.rs` moved to phase 2 once it turned out its + only consumer is `input/state.rs`. No dependency became unused, so the pruning + step is a no-op (four dependencies were already unused before this work). +- **Phases 2-5: not started.** +- One pre-existing, unrelated breakage was found; see + [A pre-existing wasm blocker](#a-pre-existing-wasm-blocker). + ## The two facts that shape the work **GPUI still comes from upstream — addressed as the `gpui-pre` package.** `gpui-base` declares its GPUI dependency as `gpui = { package = "gpui-pre", version = "0.3.1" }`: the crate in the graph is the published package `gpui-pre`, and `gpui` is only the name -used in code. That package is upstream zed's gpui (a snapshot of `zed@d89e9c2`) -republished unchanged, so nothing is forked and there is no source to align. Coop -currently pins zed's git repository at `4b47ceb` (2026-09-17), roughly four days ahead -of that snapshot. +used in code. That package is upstream zed's gpui (a snapshot of `zed@d89e9c2`, +published 2026-09-14) republished unchanged, so nothing is forked and there is no +source to align. Coop previously pinned zed's git repository at `4b47ceb` +(2026-09-17), a few days ahead of that snapshot. The two cannot be mixed. Zed's git `gpui` and the `gpui-pre` package are different crates, so `App`, `Window`, `Entity`, and elements from one are not the other's types, @@ -39,9 +52,10 @@ deeper than `ui::::`: | `notification`, `avatar`, `menu`, `scroll`, `group_box`, `indicator`, `switch`, `modal`, `tooltip` | 12 | 16 | | `list`, `checkbox`, `popover`, `resizable`, `skeleton`, `tab`, `divider` (module), `history`, `animation`, `actions` | 0 references | 0 | -`ui::list` and `ui::checkbox` have no consumers at all; the message list in -`crates/chat_ui` uses GPUI's own `list::ListState`. The modules with zero external -references still serve as internal machinery for `dock`, `menu`, `modal`, and `input`. +`ui::list` and `ui::checkbox` had no consumers at all — the message list in +`crates/chat_ui` uses GPUI's own `list::ListState` — so phase 1 deleted both. The other +modules with zero external references still serve as internal machinery for `dock`, +`menu`, `modal`, and `input`. The consequence: this is not a rewrite of an app-facing library. Most of the work is deleting internals and re-expressing a few thousand lines of presentation over base @@ -59,15 +73,36 @@ primitives. - `gpui-component` is not adopted. It is a complete, styled visual language, and taking it would replace the design system rather than preserve it. -Two `Theme` types will exist — `theme::Theme` and `gpui_base::Theme` — as separate GPUI +Two `Theme` types exist — `theme::Theme` and `gpui_base::Theme` — as separate GPUI globals. Coop's stays the application-facing one. Base's is touched in exactly one -place: a `theme::sync_base(cx)` that projects coop's colors into -`gpui_base::Theme::global_mut(cx).tokens` (`SemanticThemeTokens`: colors, radius, -typography, shadow) plus `ThemeAppearance`, `ScrollbarTheme`, and `ResizableTheme`. It -runs from `ui::init` and on every theme change. This is needed because base paints a -few things itself — the focus ring from `FocusableExt`, text selection under glyphs, -scrollbars, resize handles, and the dialog backdrop — and those should follow coop's -palette rather than base's default. +place: `theme::sync_base(cx)`, called from `ui::init` and from `Theme::change` so that +it re-runs on every theme change. It is a no-op before coop's theme global exists, +which is the case when `ui::init` runs ahead of `theme::init`; `Theme::change` is the +hook that actually keeps the projection current. + +It projects the color roles base can act on — the focus ring, the wash under selected +text, scrollbars, and overlay backdrops — and nothing else: + +| `gpui_base::ColorTokens` | coop `ThemeColors` | +| --- | --- | +| `background` / `foreground` | `background` / `text` | +| `surface` / `surface_foreground` | `surface_background` / `text` | +| `primary` / `primary_foreground` | `element_background` / `element_foreground` | +| `secondary` / `secondary_foreground` | `secondary_background` / `secondary_foreground` | +| `muted` / `muted_foreground` | `ghost_element_background_alt` / `text_muted` | +| `accent` / `accent_foreground` | `ghost_element_hover` / `text` | +| `destructive` / `destructive_foreground` | `danger_background` / `danger_foreground` | +| `border` / `input` | `border` | +| `ring` | `ring` | +| `selection` | `selection` | + +It also sets `ThemeAppearance` from coop's mode, `ScrollbarTheme`'s mode from coop's +`scrollbar_mode`, and `TypographyTokens::sans` from coop's `font_family`. Radius, +spacing, typography sizes, shadows, and scrollbar geometry keep their base defaults: +coop has a single `radius`/`radius_lg`/`font_size` where base has six-point scales, so +any mapping would be invented rather than derived. `ResizableTheme` needs nothing — +base's documented `None` fallback already resolves to `border` at rest and `ring` while +dragging, both of which are projected. ## What each module becomes @@ -85,8 +120,9 @@ palette rather than base's default. | `button.rs` | 626 | Skin: base behavior plus coop's existing variant tables | `Button`, `StateStyle` | | `switch.rs` | 287 | Skin | `Switch`, `SwitchTrack`, `SwitchThumb` | | `avatar.rs` | 141 | Skin | `Avatar`, `AvatarImage`, `AvatarFallback` | -| `history.rs`, `index_path.rs`, `element_ext.rs`, `event.rs`, `focusable.rs` | 340 | Delete | `History`/`UndoHistory`, `IndexPath`, `ElementExt`, `InteractiveElementExt`, `FocusableExt`, `FocusTrapElement` | -| `styled.rs`, `actions.rs`, `animation.rs` | 305 | Keep `ui::StyledExt`, `Size`, and `Sizable` as the app's import; base's `h_flex`/`v_flex` helpers are identical (`flex_row` + `items_center`) and can be delegated to | `styled`, `StateStyle` | +| `history.rs` | 184 | Defer to phase 2 | `UndoHistory`, not `History`: base's `History` is navigation (back/forward), while `UndoHistory` is the grouped undo/redo with `max_undos`, `group_interval`, `start_grouping`/`end_grouping`, and `set_ignoring` in place of the fork's `pub(crate) ignore` field. Its only consumer is `input/state.rs`, which phase 2 replaces | +| `index_path.rs`, `element_ext.rs`, `event.rs`, `focusable.rs` | 156 | Delete | `IndexPath`, `ElementExt`, `InteractiveElementExt`. `FocusableCycle` has no counterpart — base's `FocusableExt` is a different concept (whether a component draws a focus ring) — so it is dropped rather than re-based | +| `styled.rs`, `actions.rs`, `animation.rs` | 305 | Keep `ui::StyledExt`, `Size`, and `Sizable` as the app's import. `Selectable`, `Disableable`, and `Collapsible` now come from `gpui_base::component_traits`; the local three-line `h_flex`/`v_flex` wrappers stay rather than delegating to base's identical ones | `styled`, `StateStyle` | | `icon.rs`, `kbd.rs`, `divider.rs`, `skeleton.rs`, `group_box.rs`, `indicator.rs` | 1,023 | Keep; no base equivalent, these are the design system | — | | `menu/` | 2,208 | Keep; base has no menu. Optional later: re-base anchoring and dismissal on `Popup`/`Positioner` | `Popup` (optional) | | `dock/` + `tab/` | 3,356 | Keep for now; see phase 5 | base dock (different contract) | @@ -100,58 +136,99 @@ The workspace manifest's GPUI entries become: ```toml [workspace.dependencies] -gpui = { package = "gpui-pre", version = "0.3.5" } -gpui_platform = { package = "gpui-pre-platform", version = "0.3.5", features = ["font-kit", "x11", "wayland"] } -gpui_linux = { package = "gpui-pre-linux", version = "0.3.5" } -gpui_windows = { package = "gpui-pre-windows", version = "0.3.5" } -gpui_macos = { package = "gpui-pre-macos", version = "0.3.5" } -gpui_web = { package = "gpui-pre-web", version = "0.3.5" } -reqwest_client = { package = "gpui-pre-reqwest-client", version = "0.3.5" } -sum_tree = { package = "gpui-pre-sum-tree", version = "0.3.5" } +gpui = { package = "gpui-pre", version = "0.3.5" } +gpui_platform = { package = "gpui-pre-platform", version = "0.3.5", features = ["font-kit", "x11", "wayland"] } +gpui_linux = { package = "gpui-pre-linux", version = "0.3.5" } +gpui_windows = { package = "gpui-pre-windows", version = "0.3.5" } +gpui_macos = { package = "gpui-pre-macos", version = "0.3.5" } +gpui_web = { package = "gpui-pre-web", version = "0.3.5" } +gpui_util = { package = "gpui-pre-util", version = "0.3.5" } +reqwest_client = { package = "gpui-pre-reqwest-client", version = "0.3.5" } +sum_tree = { package = "gpui-pre-sum-tree", version = "0.3.5" } +gpui_tokio = { path = "crates/gpui_tokio" } gpui-base = "0.6.1" ``` Because of the `package =` alias, `use gpui::…` and `use gpui_platform::…` keep -compiling unchanged. `gpui_web` moves from `web/Cargo.toml` into the workspace table -with the rest. +compiling unchanged. The aliases match the ones `gpui-pre` uses internally, and +`gpui_web` moved out of `web/Cargo.toml` into this table with the rest. The only alternative — leaving the workspace on zed's git `gpui` and redirecting `gpui-base`'s dependency to it — means vendoring `gpui-base` and owning its source. That is a fork, and this plan deliberately avoids it. -`gpui_tokio` is the one missing piece: longbridge does not republish it, and -`crates/state` uses it in three places (`init`, `spawn`, `spawn_result`). Either vendor -zed's small crate into the workspace, or drop it for `cx.background_spawn`. Decide in -phase 0. +`gpui_tokio` is the one crate in the family longbridge does not republish. `crates/state` +uses it to run `browser-signer-proxy` and `nostr-blossom` work, and the nostr client's +reqwest backend needs a Tokio reactor, so the runtime cannot be dropped for +`cx.background_spawn`. It is vendored verbatim from zed at `4b47ceb` into +`crates/gpui_tokio` (Apache-2.0, ~100 lines), which is the smallest change that keeps +the existing behaviour. `gpui-base` and `gpui-pre` move together on minor versions (`0.6.x` requires `0.3.x`); bump both in the same change. ## Phases -### Phase 0 — move `gpui` onto the `gpui-pre` package (manifest only) +### Phase 0 — move `gpui` onto the `gpui-pre` package (manifest only) — landed -Point the workspace's GPUI entries at the published `gpui-pre` crates and fix whatever -the four days of API drift between `4b47ceb` and `zed@d89e9c2` broke. There is no GPUI -source to align, patch, or vendor. Confirm that the entry points coop calls still exist -in 0.3.5: `gpui_platform::application()`, `web_init()`, and `single_threaded_web()`. +Point the workspace's GPUI entries at the published `gpui-pre` crates. There is no GPUI +source to align, patch, or vendor. -Exit criteria: `cargo check` passes for `desktop` and for -`cargo check -p coop_web --target wasm32-unknown-unknown`, and the drift fixes are -listed in the pull request. The change rewrites the dependency graph, so it stays in a -pull request of its own. +**No drift had to be fixed.** The gap between the snapshot (`zed@d89e9c2`) and the old +pin (`4b47ceb`) is 67 commits, but only 16 touch the GPUI crates, and the public surface +only gained names: `ShapedLineCursor`, `MissingGlyphSink`, `MissingGlyph`, +`FallbackFontClass`, `MEASUREMENT_VERSION`, dynamic font installation, and inspector +registration. Nothing coop used was removed or changed shape, so every `use gpui::…` +compiled unchanged. The three entry points coop calls — +`gpui_platform::application()`, `gpui_platform::web_init()`, and +`gpui_platform::single_threaded_web()` — all exist in 0.3.5. -### Phase 1 — Wire base, delete dead weight (no visual change) +Exit criteria: `cargo check` passes for `desktop`, and the wasm criterion is blocked by +a pre-existing bug unrelated to GPUI — see +[A pre-existing wasm blocker](#a-pre-existing-wasm-blocker). `cargo check -p theme -p ui +--target wasm32-unknown-unknown`, which covers everything this migration touches, +passes. The change rewrites the dependency graph, so it stays in a pull request of its +own. -Add `gpui-base`, make `ui::init` call `gpui_base::init(cx)` followed by -`theme::sync_base(cx)`, and re-export the base utilities the app already imports under -their current names (`ElementExt`, `InteractiveElementExt`, `IndexPath`, `History`, -`Disableable`, `Selectable`). Delete `checkbox.rs` and `list/`, which have no -consumers, along with `history.rs`, `index_path.rs`, `element_ext.rs`, and `event.rs` -once base supplies them. Drop the dependencies this leaves unused. +### Phase 1 — Wire base, delete dead weight (no visual change) — landed -Exit criteria: no diff outside `crates/ui` and `crates/theme`, the app launches, and -switching the theme still restyles everything. +What changed: + +- `crates/ui` and `crates/theme` take `gpui-base`. +- `ui::init` calls `gpui_base::init(cx)` then `theme::sync_base(cx)`; the `list::init(cx)` + call went with `list/`. +- `ui`'s crate root re-exports `ElementExt`, `IndexPath`, and `InteractiveElementExt` + from `gpui_base`, so existing `use ui::{…}` sites are unchanged. In particular + `chat_ui`'s `.on_double_click(…)` is served by base's `InteractiveElementExt`, which + is the same implementation as the fork's. +- `ui::styled` no longer defines `Selectable`, `Disableable`, or `Collapsible`; it + re-exports them from `gpui_base::component_traits`. All three are signature-identical + to the fork's, so the `impl` blocks in `avatar`, `button`, `input`, and the rest + compile untouched. The path is `component_traits` rather than the crate root because + `gpui_base::Collapsible` is base's *component* of that name, not the trait. +- Deleted: `checkbox.rs` (312), `list/` (1,477), `index_path.rs` (69), + `element_ext.rs` (27), `event.rs` (21), `focusable.rs` (39) — 1,945 lines, with no + external consumers and a base counterpart for everything except `FocusableCycle`. + +Two corrections this phase produced: + +- **`history.rs` moved to phase 2.** It maps to base's `UndoHistory`, not `History`: + base's `History` is navigation (back/forward), while `UndoHistory` is the grouped + undo/redo. Swapping it means editing `input/state.rs` — six `ignore` writes become + `set_ignoring`, and `Change` loses its `HistoryItem` impl — which is phase 2's file. +- **No dependency became unused.** `ropey`, `sum_tree`, `lsp-types`, `tree-sitter`, + `regex`, `unicode-segmentation`, `uuid`, and `instant` are all still used by `input/` + and `history.rs`, and the deleted files used none of the others, so pruning happens in + phase 2. Separately, four dependencies — `common`, `anyhow`, `itertools`, and `smol` — + were already unreferenced anywhere in `crates/ui/src` *before* this change. They are + left alone here because removing them is unrelated to the migration. + +Exit criteria: no diff outside `crates/ui` and `crates/theme` — met; the only files +touched are the two manifests, `ui/src/lib.rs`, `ui/src/styled.rs`, and +`theme/src/lib.rs`. `cargo check` and `cargo build` both pass with no warnings, and +`theme` and `ui` still compile for `wasm32-unknown-unknown`. The remaining part of the +acceptance — launching the app and walking the settings dialog and chat panel — has to +be done by hand and has not been run. ### Phase 2 — `input/` (the largest single win, ~6.9k lines) @@ -213,8 +290,10 @@ menu positioning and dismissal on base `Popup`/`Positioner` is optional and late There is no UI test suite to lean on, so each phase gets the same treatment: -- `cargo check` at the workspace root, plus - `cargo check -p coop_web --target wasm32-unknown-unknown` for the web target. +- `cargo check` and `cargo build` at the workspace root. +- `cargo check -p theme -p ui --target wasm32-unknown-unknown`. The web target cannot + be checked end to end until the pre-existing blocker below is fixed, so the migrated + crates are checked directly. - Launch the app and walk the surfaces the phase touched. The settings dialog is the densest single smoke surface (Button, GroupBox, Switch, Input, DropdownMenu, PopupMenuItem), followed by the chat panel and the sidebar. @@ -223,6 +302,22 @@ There is no UI test suite to lean on, so each phase gets the same treatment: because base has no equivalent (`set_loading` is the known candidate), list it in the pull request. +### A pre-existing wasm blocker + +`cargo check -p coop_web --target wasm32-unknown-unknown` fails while compiling +`errno 0.3.14`, which refuses `wasm32-unknown-unknown`. The path is +`coop_web → workspace → browser-signer-proxy → smol → async-io → rustix → errno`, none +of which involves GPUI. `crates/workspace/Cargo.toml` declares `browser-signer-proxy`, +but nothing under `crates/workspace/src` references it; the crate is only used by +`crates/state`, where it is already gated `#[cfg(not(target_arch = "wasm32"))]`. + +Every version on that path (`errno 0.3.14`, `rustix 1.1.5`, `async-io 2.6.0`, +`smol 2.0.2`) is identical before and after phase 0, and no file on it is part of this +work, so the web build was already broken. The remedy is deleting that one stale +dependency line, but that is unrelated to the migration and is deliberately left out. +Until it is done, read the wasm exit criterion for phases 1-4 as "`theme` and `ui` +compile for `wasm32-unknown-unknown`". + ## Risks and non-goals - **Snapshot lag.** The `gpui-pre` package is a republished snapshot, so it trails zed @@ -236,21 +331,24 @@ There is no UI test suite to lean on, so each phase gets the same treatment: - **Two `Theme` globals.** Confine `gpui_base::Theme` to `theme::sync_base` and `crates/ui` internals; application code keeps using `theme::ActiveTheme`. Avoid importing both `Theme` types into one file. -- **`gpui_tokio`** has to be vendored or dropped (phase 0). +- **`gpui_tokio` is vendored, not ours.** `crates/gpui_tokio` is zed's crate kept + verbatim at `crates/gpui_tokio/src/lib.rs` because the `gpui-pre` family does not + publish it and the nostr client needs a Tokio reactor. Re-sync or delete it if + longbridge ever ships an equivalent. - **Non-goals:** adopting `gpui-component`, migrating dock/tab/menu, rewriting the self-contained pieces (`icon`, `kbd`, `divider`, `skeleton`, `group_box`, `indicator`), and changing any color, radius, or spacing value. ## Pull request sequence -| PR | Content | Touches outside `crates/ui` | -| --- | --- | --- | -| 1 | Phase 0: move `gpui` to the `gpui-pre` package, fix drift | `Cargo.toml`, possibly `crates/state` | -| 2 | Phase 1: base wiring, `sync_base`, deletions | none | -| 3 | Phase 2: input | none, or the named gaps above | -| 4 | Phase 3: popover, modal, notification, tooltip | none | -| 5–10 | Phase 4: one leaf module each | none | -| later | Phase 5: dock, as its own plan | `crates/workspace`, `crates/chat_ui` | +| PR | Content | Touches outside `crates/ui` | Status | +| --- | --- | --- | --- | +| 1 | Phase 0: `gpui` moves to the `gpui-pre` package, `gpui_tokio` vendored | root `Cargo.toml`, `Cargo.lock`, `web/Cargo.toml`, new `crates/gpui_tokio`; `crates/state` needed no edit | landed | +| 2 | Phase 1: base wiring, `sync_base`, deletions | `crates/theme` | landed | +| 3 | Phase 2: input, plus `history.rs` → `UndoHistory` and the `ropey`/`sum_tree`/… pruning | none, or the named gaps | not started | +| 4 | Phase 3: popover, modal, notification, tooltip | none | not started | +| 5–10 | Phase 4: one leaf module each | none | not started | +| later | Phase 5: dock, as its own plan | `crates/workspace`, `crates/chat_ui` | not started | The end state: the application keeps its design system and its call sites, `crates/ui` shrinks by roughly half, and the parts that are genuinely hard — text editing, diff --git a/web/Cargo.toml b/web/Cargo.toml index 69597762..724ec6ae 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -21,7 +21,7 @@ person = { path = "../crates/person" } gpui.workspace = true gpui_platform.workspace = true -gpui_web = { git = "https://github.com/zed-industries/zed" } +gpui_web.workspace = true log.workspace = true instant = { workspace = true, features = ["wasm-bindgen"] }