Merge branch 'main' into user-menu

Conrad Irwin created

Change summary

Cargo.lock                                           | 135 +
Cargo.toml                                           |   1 
crates/auto_update2/src/auto_update.rs               |  19 
crates/auto_update2/src/update_notification.rs       |   6 
crates/collab_ui2/Cargo.toml                         |   2 
crates/collab_ui2/src/chat_panel.rs                  |  28 
crates/collab_ui2/src/collab_panel.rs                | 318 ++--
crates/collab_ui2/src/collab_panel/contact_finder.rs |  39 
crates/collab_ui2/src/collab_titlebar_item.rs        | 170 -
crates/collab_ui2/src/collab_ui.rs                   |   2 
crates/collab_ui2/src/face_pile.rs                   |   2 
crates/editor2/src/editor.rs                         |  26 
crates/editor2/src/element.rs                        | 356 ++---
crates/feedback2/Cargo.toml                          |  13 
crates/feedback2/src/feedback_modal.rs               | 238 ++-
crates/gpui2/src/app.rs                              |   7 
crates/gpui2/src/element.rs                          |  95 
crates/gpui2/src/elements/canvas.rs                  |  16 
crates/gpui2/src/elements/div.rs                     | 711 ++++++++---
crates/gpui2/src/elements/img.rs                     |   5 
crates/gpui2/src/elements/list.rs                    |   4 
crates/gpui2/src/elements/overlay.rs                 |   4 
crates/gpui2/src/elements/svg.rs                     |   8 
crates/gpui2/src/elements/text.rs                    |  15 
crates/gpui2/src/elements/uniform_list.rs            |  73 
crates/gpui2/src/geometry.rs                         |  33 
crates/gpui2/src/platform.rs                         |   1 
crates/gpui2/src/platform/mac/metal_renderer.rs      |   6 
crates/gpui2/src/platform/mac/window.rs              |  27 
crates/gpui2/src/platform/test/window.rs             |   4 
crates/gpui2/src/style.rs                            |  51 
crates/gpui2/src/styled.rs                           |  14 
crates/gpui2/src/text_system/line.rs                 |  26 
crates/gpui2/src/view.rs                             |  42 
crates/gpui2/src/window.rs                           | 222 ++
crates/gpui2_macros/src/derive_render.rs             |  23 
crates/gpui2_macros/src/gpui2_macros.rs              |   6 
crates/project_panel2/src/project_panel.rs           |  37 
crates/search2/src/project_search.rs                 | 281 ++--
crates/storybook2/Cargo.toml                         |   3 
crates/storybook2/src/stories/z_index.rs             |   4 
crates/storybook2/src/storybook2.rs                  |  18 
crates/terminal_view2/src/terminal_element.rs        | 316 ++--
crates/theme2/src/themes/andromeda.rs                |   2 
crates/theme2/src/themes/ayu.rs                      |   3 
crates/theme2/src/themes/dracula.rs                  |   1 
crates/theme2/src/themes/gruvbox.rs                  |   6 
crates/theme2/src/themes/night_owl.rs                |   2 
crates/theme2/src/themes/noctis.rs                   |  11 
crates/theme2/src/themes/nord.rs                     |   1 
crates/theme2/src/themes/palenight.rs                |   3 
crates/theme2/src/themes/rose_pine.rs                |   3 
crates/theme2/src/themes/solarized.rs                |   2 
crates/theme_importer/src/theme_printer.rs           |   1 
crates/theme_importer/src/vscode/converter.rs        |  27 
crates/ui2/src/components/button/button_like.rs      |  13 
crates/ui2/src/components/button/icon_button.rs      |   7 
crates/ui2/src/components/disclosure.rs              |   3 
crates/ui2/src/components/list/list_header.rs        |   3 
crates/ui2/src/components/list/list_item.rs          |  30 
crates/ui2/src/components/popover_menu.rs            |   6 
crates/ui2/src/components/right_click_menu.rs        |   8 
crates/ui2/src/components/stories/list_item.rs       |   4 
crates/ui2/src/components/tab.rs                     |   3 
crates/ui2/src/prelude.rs                            |   1 
crates/ui2/src/styles/elevation.rs                   |  12 
crates/ui2/src/ui2.rs                                |   2 
crates/ui2/src/visible_on_hover.rs                   |  15 
crates/vcs_menu2/Cargo.toml                          |  17 
crates/vcs_menu2/src/lib.rs                          | 359 +++++
crates/workspace2/src/dock.rs                        | 255 ---
crates/workspace2/src/item.rs                        |   6 
crates/workspace2/src/modal_layer.rs                 |   8 
crates/workspace2/src/notifications.rs               |  43 
crates/workspace2/src/pane.rs                        | 293 ++-
crates/workspace2/src/pane_group.rs                  | 420 ------
crates/workspace2/src/searchable.rs                  |   4 
crates/workspace2/src/status_bar.rs                  |   4 
crates/workspace2/src/toolbar.rs                     |  28 
crates/workspace2/src/workspace2.rs                  | 884 ++++++-------
crates/zed2/src/zed2.rs                              |   2 
debug.plist                                          |   8 
docker-compose.sql                                   |   1 
docker-compose.yml                                   |  16 
script/storybook                                     |   7 
85 files changed, 3,279 insertions(+), 2,652 deletions(-)

Detailed changes

Cargo.lock πŸ”—

@@ -182,7 +182,7 @@ dependencies = [
  "alacritty_config",
  "alacritty_config_derive",
  "base64 0.13.1",
- "bitflags 2.4.0",
+ "bitflags 2.4.1",
  "home",
  "libc",
  "log",
@@ -200,7 +200,7 @@ dependencies = [
  "toml 0.7.8",
  "unicode-width",
  "vte",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -293,7 +293,7 @@ version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
 dependencies = [
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -303,7 +303,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd"
 dependencies = [
  "anstyle",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -580,7 +580,7 @@ dependencies = [
  "futures-lite",
  "rustix 0.37.23",
  "signal-hook",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -1025,9 +1025,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
 
 [[package]]
 name = "bitflags"
-version = "2.4.0"
+version = "2.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635"
+checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
 dependencies = [
  "serde",
 ]
@@ -1985,6 +1985,7 @@ dependencies = [
  "tree-sitter-markdown",
  "ui2",
  "util",
+ "vcs_menu2",
  "workspace2",
  "zed_actions2",
 ]
@@ -2087,6 +2088,19 @@ dependencies = [
  "crossbeam-utils",
 ]
 
+[[package]]
+name = "console"
+version = "0.15.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8"
+dependencies = [
+ "encode_unicode",
+ "lazy_static",
+ "libc",
+ "unicode-width",
+ "windows-sys 0.45.0",
+]
+
 [[package]]
 name = "const-cstr"
 version = "0.3.0"
@@ -2574,7 +2588,7 @@ dependencies = [
  "openssl-sys",
  "pkg-config",
  "vcpkg",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -2783,6 +2797,20 @@ dependencies = [
  "workspace2",
 ]
 
+[[package]]
+name = "dialoguer"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de"
+dependencies = [
+ "console",
+ "fuzzy-matcher",
+ "shell-words",
+ "tempfile",
+ "thiserror",
+ "zeroize",
+]
+
 [[package]]
 name = "diff"
 version = "0.1.13"
@@ -3028,6 +3056,12 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "encode_unicode"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
+
 [[package]]
 name = "encoding_rs"
 version = "0.8.33"
@@ -3106,7 +3140,7 @@ checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd"
 dependencies = [
  "errno-dragonfly",
  "libc",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -3137,7 +3171,7 @@ checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943"
 dependencies = [
  "cfg-if 1.0.0",
  "home",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -3249,6 +3283,7 @@ name = "feedback2"
 version = "0.1.0"
 dependencies = [
  "anyhow",
+ "bitflags 2.4.1",
  "client2",
  "db2",
  "editor2",
@@ -3268,6 +3303,7 @@ dependencies = [
  "serde_derive",
  "settings2",
  "smallvec",
+ "smol",
  "sysinfo",
  "theme2",
  "tree-sitter-markdown",
@@ -3344,7 +3380,7 @@ dependencies = [
  "cfg-if 1.0.0",
  "libc",
  "redox_syscall 0.3.5",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -3723,6 +3759,15 @@ dependencies = [
  "util",
 ]
 
+[[package]]
+name = "fuzzy-matcher"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94"
+dependencies = [
+ "thread_local",
+]
+
 [[package]]
 name = "fuzzy2"
 version = "0.1.0"
@@ -3985,7 +4030,7 @@ dependencies = [
  "async-task",
  "backtrace",
  "bindgen 0.65.1",
- "bitflags 2.4.0",
+ "bitflags 2.4.1",
  "block",
  "cbindgen",
  "cocoa",
@@ -4244,7 +4289,7 @@ version = "0.5.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb"
 dependencies = [
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -4521,7 +4566,7 @@ checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
 dependencies = [
  "hermit-abi 0.3.3",
  "libc",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -4578,7 +4623,7 @@ checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
 dependencies = [
  "hermit-abi 0.3.3",
  "rustix 0.38.14",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -5001,7 +5046,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d580318f95776505201b28cf98eb1fa5e4be3b689633ba6a3e6cd880ff22d8cb"
 dependencies = [
  "cfg-if 1.0.0",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -5494,7 +5539,7 @@ checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
 dependencies = [
  "libc",
  "wasi 0.11.0+wasi-snapshot-preview1",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -6154,7 +6199,7 @@ version = "0.10.57"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c"
 dependencies = [
- "bitflags 2.4.0",
+ "bitflags 2.4.1",
  "cfg-if 1.0.0",
  "foreign-types",
  "libc",
@@ -6675,7 +6720,7 @@ dependencies = [
  "libc",
  "log",
  "pin-project-lite 0.2.13",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -7874,7 +7919,7 @@ version = "0.29.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2"
 dependencies = [
- "bitflags 2.4.0",
+ "bitflags 2.4.1",
  "fallible-iterator",
  "fallible-streaming-iterator",
  "hashlink",
@@ -7981,7 +8026,7 @@ dependencies = [
  "io-lifetimes 1.0.11",
  "libc",
  "linux-raw-sys 0.3.8",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -7990,11 +8035,11 @@ version = "0.38.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "747c788e9ce8e92b12cd485c49ddf90723550b654b32508f979b71a7b1ecda4f"
 dependencies = [
- "bitflags 2.4.0",
+ "bitflags 2.4.1",
  "errno 0.3.3",
  "libc",
  "linux-raw-sys 0.4.7",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -8107,7 +8152,7 @@ version = "0.1.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88"
 dependencies = [
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -8707,6 +8752,12 @@ dependencies = [
  "lazy_static",
 ]
 
+[[package]]
+name = "shell-words"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
+
 [[package]]
 name = "shellexpand"
 version = "2.1.2"
@@ -8906,7 +8957,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e"
 dependencies = [
  "libc",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -9087,7 +9138,7 @@ dependencies = [
  "atoi",
  "base64 0.21.4",
  "bigdecimal",
- "bitflags 2.4.0",
+ "bitflags 2.4.1",
  "byteorder",
  "bytes 1.5.0",
  "chrono",
@@ -9134,7 +9185,7 @@ dependencies = [
  "atoi",
  "base64 0.21.4",
  "bigdecimal",
- "bitflags 2.4.0",
+ "bitflags 2.4.1",
  "byteorder",
  "chrono",
  "crc",
@@ -9222,6 +9273,7 @@ dependencies = [
  "backtrace-on-stack-overflow",
  "chrono",
  "clap 4.4.4",
+ "dialoguer",
  "editor2",
  "fuzzy2",
  "gpui2",
@@ -9522,7 +9574,7 @@ dependencies = [
  "fastrand 2.0.0",
  "redox_syscall 0.3.5",
  "rustix 0.38.14",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -9961,7 +10013,7 @@ dependencies = [
  "signal-hook-registry",
  "socket2 0.5.4",
  "tokio-macros",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -10875,6 +10927,20 @@ dependencies = [
  "workspace",
 ]
 
+[[package]]
+name = "vcs_menu2"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "fs2",
+ "fuzzy2",
+ "gpui2",
+ "picker2",
+ "ui2",
+ "util",
+ "workspace2",
+]
+
 [[package]]
 name = "version_check"
 version = "0.9.4"
@@ -11577,6 +11643,15 @@ dependencies = [
  "windows-targets 0.48.5",
 ]
 
+[[package]]
+name = "windows-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets 0.42.2",
+]
+
 [[package]]
 name = "windows-sys"
 version = "0.48.0"
@@ -11716,7 +11791,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
 dependencies = [
  "cfg-if 1.0.0",
- "windows-sys",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]

Cargo.toml πŸ”—

@@ -123,6 +123,7 @@ members = [
     "crates/story",
     "crates/vim",
     "crates/vcs_menu",
+    "crates/vcs_menu2",
     "crates/workspace2",
     "crates/welcome",
     "crates/welcome2",

crates/auto_update2/src/auto_update.rs πŸ”—

@@ -9,12 +9,14 @@ use gpui::{
     ViewContext, VisualContext, WindowContext,
 };
 use isahc::AsyncBody;
+
 use serde::Deserialize;
 use serde_derive::Serialize;
 use smol::io::AsyncReadExt;
 
 use settings::{Settings, SettingsStore};
 use smol::{fs::File, process::Command};
+
 use std::{ffi::OsString, sync::Arc, time::Duration};
 use update_notification::UpdateNotification;
 use util::channel::{AppCommitSha, ReleaseChannel};
@@ -24,16 +26,7 @@ use workspace::Workspace;
 const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification";
 const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60);
 
-//todo!(remove CheckThatAutoUpdaterWorks)
-actions!(
-    auto_update,
-    [
-        Check,
-        DismissErrorMessage,
-        ViewReleaseNotes,
-        CheckThatAutoUpdaterWorks
-    ]
-);
+actions!(auto_update, [Check, DismissErrorMessage, ViewReleaseNotes]);
 
 #[derive(Serialize)]
 struct UpdateRequestBody {
@@ -90,7 +83,10 @@ pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppCo
     cx.observe_new_views(|workspace: &mut Workspace, _cx| {
         workspace.register_action(|_, action: &Check, cx| check(action, cx));
 
+        workspace.register_action(|_, action, cx| view_release_notes(action, cx));
+
         // @nate - code to trigger update notification on launch
+        // todo!("remove this when Nate is done")
         // workspace.show_notification(0, _cx, |cx| {
         //     cx.build_view(|_| UpdateNotification::new(SemanticVersion::from_str("1.1.1").unwrap()))
         // });
@@ -119,9 +115,6 @@ pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppCo
             updater
         });
         cx.set_global(Some(auto_updater));
-        //todo!(action)
-        // cx.add_global_action(view_release_notes);
-        // cx.add_action(UpdateNotification::dismiss);
     }
 }
 

crates/auto_update2/src/update_notification.rs πŸ”—

@@ -2,6 +2,7 @@ use gpui::{
     div, DismissEvent, Div, EventEmitter, InteractiveElement, ParentElement, Render,
     SemanticVersion, StatefulInteractiveElement, Styled, ViewContext,
 };
+use menu::Cancel;
 use util::channel::ReleaseChannel;
 use workspace::ui::{h_stack, v_stack, Icon, IconElement, Label, StyledExt};
 
@@ -18,6 +19,7 @@ impl Render for UpdateNotification {
         let app_name = cx.global::<ReleaseChannel>().display_name();
 
         v_stack()
+            .on_action(cx.listener(UpdateNotification::dismiss))
             .elevation_3(cx)
             .p_4()
             .child(
@@ -32,7 +34,7 @@ impl Render for UpdateNotification {
                             .id("cancel")
                             .child(IconElement::new(Icon::Close))
                             .cursor_pointer()
-                            .on_click(cx.listener(|this, _, cx| this.dismiss(cx))),
+                            .on_click(cx.listener(|this, _, cx| this.dismiss(&menu::Cancel, cx))),
                     ),
             )
             .child(
@@ -50,7 +52,7 @@ impl UpdateNotification {
         Self { version }
     }
 
-    pub fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
+    pub fn dismiss(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
         cx.emit(DismissEvent);
     }
 }

crates/collab_ui2/Cargo.toml πŸ”—

@@ -47,7 +47,7 @@ settings = { package = "settings2", path = "../settings2" }
 feature_flags = { package = "feature_flags2", path = "../feature_flags2"}
 theme = { package = "theme2", path = "../theme2" }
 theme_selector = { package = "theme_selector2", path = "../theme_selector2" }
-# vcs_menu = { path = "../vcs_menu" }
+vcs_menu = { package = "vcs_menu2", path = "../vcs_menu2" }
 ui = { package = "ui2", path = "../ui2" }
 util = { path = "../util" }
 workspace = { package = "workspace2", path = "../workspace2" }

crates/collab_ui2/src/chat_panel.rs πŸ”—

@@ -21,10 +21,7 @@ use settings::{Settings, SettingsStore};
 use std::sync::Arc;
 use theme::ActiveTheme as _;
 use time::{OffsetDateTime, UtcOffset};
-use ui::{
-    h_stack, prelude::WindowContext, v_stack, Avatar, Button, ButtonCommon as _, Clickable, Icon,
-    IconButton, Label, Tooltip,
-};
+use ui::{prelude::*, Avatar, Button, Icon, IconButton, Label, Tooltip};
 use util::{ResultExt, TryFutureExt};
 use workspace::{
     dock::{DockPosition, Panel, PanelEvent},
@@ -382,13 +379,18 @@ impl ChatPanel {
             .child(text.element("body".into(), cx))
             .child(
                 div()
-                    .invisible()
                     .absolute()
                     .top_1()
                     .right_2()
                     .w_8()
-                    .group_hover("", |this| this.visible())
-                    .child(render_remove(message_id_to_remove, cx)),
+                    .visible_on_hover("")
+                    .children(message_id_to_remove.map(|message_id| {
+                        IconButton::new(("remove", message_id), Icon::XCircle).on_click(
+                            cx.listener(move |this, _, cx| {
+                                this.remove_message(message_id, cx);
+                            }),
+                        )
+                    })),
             )
             .into_any()
     }
@@ -528,18 +530,6 @@ impl ChatPanel {
     }
 }
 
-fn render_remove(message_id_to_remove: Option<u64>, cx: &mut ViewContext<ChatPanel>) -> AnyElement {
-    if let Some(message_id) = message_id_to_remove {
-        IconButton::new(("remove", message_id), Icon::XCircle)
-            .on_click(cx.listener(move |this, _, cx| {
-                this.remove_message(message_id, cx);
-            }))
-            .into_any_element()
-    } else {
-        div().into_any_element()
-    }
-}
-
 impl EventEmitter<Event> for ChatPanel {}
 
 impl Render for ChatPanel {

crates/collab_ui2/src/collab_panel.rs πŸ”—

@@ -175,12 +175,12 @@ use editor::Editor;
 use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
 use fuzzy::{match_strings, StringMatchCandidate};
 use gpui::{
-    actions, canvas, div, img, impl_actions, overlay, point, prelude::*, px, rems, serde_json,
-    size, Action, AppContext, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div,
-    EventEmitter, FocusHandle, Focusable, FocusableView, Hsla, InteractiveElement, IntoElement,
-    Length, Model, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Quad, Render,
-    RenderOnce, ScrollHandle, SharedString, Size, Stateful, Styled, Subscription, Task, View,
-    ViewContext, VisualContext, WeakView,
+    actions, canvas, div, fill, img, impl_actions, overlay, point, prelude::*, px, rems,
+    serde_json, size, Action, AnyElement, AppContext, AsyncWindowContext, Bounds, ClipboardItem,
+    DismissEvent, Div, EventEmitter, FocusHandle, Focusable, FocusableView, Hsla,
+    InteractiveElement, IntoElement, Length, Model, MouseDownEvent, ParentElement, Pixels, Point,
+    PromptLevel, Quad, Render, RenderOnce, ScrollHandle, SharedString, Size, Stateful, Styled,
+    Subscription, Task, View, ViewContext, VisualContext, WeakView,
 };
 use project::{Fs, Project};
 use serde_derive::{Deserialize, Serialize};
@@ -402,7 +402,7 @@ impl CollabPanel {
 
             let filter_editor = cx.build_view(|cx| {
                 let mut editor = Editor::single_line(cx);
-                editor.set_placeholder_text("Filter channels, contacts", cx);
+                editor.set_placeholder_text("Filter...", cx);
                 editor
             });
 
@@ -1157,24 +1157,20 @@ impl CollabPanel {
 
         ListItem::new(SharedString::from(user.github_login.clone()))
             .start_slot(Avatar::new(user.avatar_uri.clone()))
-            .child(
-                h_stack()
-                    .w_full()
-                    .justify_between()
-                    .child(Label::new(user.github_login.clone()))
-                    .child(if is_pending {
-                        Label::new("Calling").color(Color::Muted).into_any_element()
-                    } else if is_current_user {
-                        IconButton::new("leave-call", Icon::ArrowRight)
-                            .on_click(cx.listener(move |this, _, cx| {
-                                Self::leave_call(cx);
-                            }))
-                            .tooltip(|cx| Tooltip::text("Leave Call", cx))
-                            .into_any_element()
-                    } else {
-                        div().into_any_element()
-                    }),
-            )
+            .child(Label::new(user.github_login.clone()))
+            .end_slot(if is_pending {
+                Label::new("Calling").color(Color::Muted).into_any_element()
+            } else if is_current_user {
+                IconButton::new("leave-call", Icon::Exit)
+                    .style(ButtonStyle::Subtle)
+                    .on_click(cx.listener(move |this, _, cx| {
+                        Self::leave_call(cx);
+                    }))
+                    .tooltip(|cx| Tooltip::text("Leave Call", cx))
+                    .into_any_element()
+            } else {
+                div().into_any_element()
+            })
             .when_some(peer_id, |this, peer_id| {
                 this.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
                     .on_click(cx.listener(move |this, _, cx| {
@@ -1212,8 +1208,12 @@ impl CollabPanel {
                         .detach_and_log_err(cx);
                 });
             }))
-            .start_slot(render_tree_branch(is_last, cx))
-            .child(IconButton::new(0, Icon::Folder))
+            .start_slot(
+                h_stack()
+                    .gap_1()
+                    .child(render_tree_branch(is_last, cx))
+                    .child(IconButton::new(0, Icon::Folder)),
+            )
             .child(Label::new(project_name.clone()))
             .tooltip(move |cx| Tooltip::text(format!("Open {}", project_name), cx))
 
@@ -1305,8 +1305,12 @@ impl CollabPanel {
         let id = peer_id.map_or(usize::MAX, |id| id.as_u64() as usize);
 
         ListItem::new(("screen", id))
-            .start_slot(render_tree_branch(is_last, cx))
-            .child(IconButton::new(0, Icon::Screen))
+            .start_slot(
+                h_stack()
+                    .gap_1()
+                    .child(render_tree_branch(is_last, cx))
+                    .child(IconButton::new(0, Icon::Screen)),
+            )
             .child(Label::new("Screen"))
             .when_some(peer_id, |this, _| {
                 this.on_click(cx.listener(move |this, _, cx| {
@@ -1372,9 +1376,13 @@ impl CollabPanel {
             .on_click(cx.listener(move |this, _, cx| {
                 this.open_channel_notes(channel_id, cx);
             }))
-            .start_slot(render_tree_branch(false, cx))
-            .child(IconButton::new(0, Icon::File))
-            .child(Label::new("notes"))
+            .start_slot(
+                h_stack()
+                    .gap_1()
+                    .child(render_tree_branch(false, cx))
+                    .child(IconButton::new(0, Icon::File)),
+            )
+            .child(div().h_7().w_full().child(Label::new("notes")))
             .tooltip(move |cx| Tooltip::text("Open Channel Notes", cx))
     }
 
@@ -1387,8 +1395,12 @@ impl CollabPanel {
             .on_click(cx.listener(move |this, _, cx| {
                 this.join_channel_chat(channel_id, cx);
             }))
-            .start_slot(render_tree_branch(true, cx))
-            .child(IconButton::new(0, Icon::MessageBubbles))
+            .start_slot(
+                h_stack()
+                    .gap_1()
+                    .child(render_tree_branch(false, cx))
+                    .child(IconButton::new(0, Icon::MessageBubbles)),
+            )
             .child(Label::new("chat"))
             .tooltip(move |cx| Tooltip::text("Open Chat", cx))
     }
@@ -2130,7 +2142,7 @@ impl CollabPanel {
     }
 
     fn render_signed_out(&mut self, cx: &mut ViewContext<Self>) -> Div {
-        v_stack().child(
+        v_stack().border_1().border_color(gpui::red()).child(
             Button::new("sign_in", "Sign in to collaborate").on_click(cx.listener(
                 |this, _, cx| {
                     let client = this.client.clone();
@@ -2149,11 +2161,6 @@ impl CollabPanel {
     fn render_signed_in(&mut self, cx: &mut ViewContext<Self>) -> Div {
         v_stack()
             .size_full()
-            .child(
-                div()
-                    .p_2()
-                    .child(div().rounded(px(2.0)).child(self.filter_editor.clone())),
-            )
             .child(
                 v_stack()
                     .size_full()
@@ -2223,6 +2230,14 @@ impl CollabPanel {
                         }
                     })),
             )
+            .child(
+                div().p_2().child(
+                    div()
+                        .border_primary(cx)
+                        .border_t()
+                        .child(self.filter_editor.clone()),
+                ),
+            )
     }
 
     fn render_header(
@@ -2275,21 +2290,32 @@ impl CollabPanel {
             Section::ActiveCall => channel_link.map(|channel_link| {
                 let channel_link_copy = channel_link.clone();
                 IconButton::new("channel-link", Icon::Copy)
+                    .icon_size(IconSize::Small)
+                    .size(ButtonSize::None)
+                    .visible_on_hover("section-header")
                     .on_click(move |_, cx| {
                         let item = ClipboardItem::new(channel_link_copy.clone());
                         cx.write_to_clipboard(item)
                     })
                     .tooltip(|cx| Tooltip::text("Copy channel link", cx))
+                    .into_any_element()
             }),
             Section::Contacts => Some(
-                IconButton::new("add-contact", Icon::Plus)
-                    .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx)))
-                    .tooltip(|cx| Tooltip::text("Search for new contact", cx)),
+                div()
+                    .border_1()
+                    .border_color(gpui::red())
+                    .child(
+                        IconButton::new("add-contact", Icon::Plus)
+                            .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx)))
+                            .tooltip(|cx| Tooltip::text("Search for new contact", cx)),
+                    )
+                    .into_any_element(),
             ),
             Section::Channels => Some(
                 IconButton::new("add-channel", Icon::Plus)
                     .on_click(cx.listener(|this, _, cx| this.new_root_channel(cx)))
-                    .tooltip(|cx| Tooltip::text("Create a channel", cx)),
+                    .tooltip(|cx| Tooltip::text("Create a channel", cx))
+                    .into_any_element(),
             ),
             _ => None,
         };
@@ -2302,27 +2328,20 @@ impl CollabPanel {
             | Section::Offline => true,
         };
 
-        h_stack()
+        let mut row = h_stack()
             .w_full()
-            .map(|el| {
-                if can_collapse {
-                    el.child(
-                        ListItem::new(text.clone())
-                            .child(div().w_full().child(Label::new(text)))
-                            .selected(is_selected)
-                            .toggle(Some(!is_collapsed))
-                            .on_click(cx.listener(move |this, _, cx| {
-                                this.toggle_section_expanded(section, cx)
-                            })),
-                    )
-                } else {
-                    el.child(
-                        ListHeader::new(text)
-                            .when_some(button, |el, button| el.end_slot(button))
-                            .selected(is_selected),
-                    )
-                }
-            })
+            .group("section-header")
+            .child(
+                ListHeader::new(text)
+                    .toggle(if can_collapse {
+                        Some(!is_collapsed)
+                    } else {
+                        None
+                    })
+                    .inset(true)
+                    .end_slot::<AnyElement>(button)
+                    .selected(is_selected),
+            )
             .when(section == Section::Channels, |el| {
                 el.drag_over::<DraggedChannelView>(|style| {
                     style.bg(cx.theme().colors().ghost_element_hover)
@@ -2336,7 +2355,13 @@ impl CollabPanel {
                             .detach_and_log_err(cx)
                     },
                 ))
-            })
+            });
+
+        if section == Section::Offline {
+            row = div().border_1().border_color(gpui::red()).child(row);
+        }
+
+        row
     }
 
     fn render_contact(
@@ -2363,21 +2388,16 @@ impl CollabPanel {
                         })
                         .when(!calling, |el| {
                             el.child(
-                                div()
-                                    .id("remove_contact")
-                                    .invisible()
-                                    .group_hover("", |style| style.visible())
-                                    .child(
-                                        IconButton::new("remove_contact", Icon::Close)
-                                            .icon_color(Color::Muted)
-                                            .tooltip(|cx| Tooltip::text("Remove Contact", cx))
-                                            .on_click(cx.listener({
-                                                let github_login = github_login.clone();
-                                                move |this, _, cx| {
-                                                    this.remove_contact(user_id, &github_login, cx);
-                                                }
-                                            })),
-                                    ),
+                                IconButton::new("remove_contact", Icon::Close)
+                                    .icon_color(Color::Muted)
+                                    .visible_on_hover("")
+                                    .tooltip(|cx| Tooltip::text("Remove Contact", cx))
+                                    .on_click(cx.listener({
+                                        let github_login = github_login.clone();
+                                        move |this, _, cx| {
+                                            this.remove_contact(user_id, &github_login, cx);
+                                        }
+                                    })),
                             )
                         }),
                 )
@@ -2460,7 +2480,7 @@ impl CollabPanel {
                     .child(Label::new(github_login.clone()))
                     .child(h_stack().children(controls)),
             )
-            .start_slot::<Avatar>(user.avatar_uri.clone().map(|avatar| Avatar::new(avatar)))
+            .start_slot(Avatar::new(user.avatar_uri.clone()))
     }
 
     fn render_contact_placeholder(
@@ -2541,12 +2561,13 @@ impl CollabPanel {
         div()
             .id(channel_id as usize)
             .group("")
-            .on_drag({
-                let channel = channel.clone();
-                move |cx| {
-                    let channel = channel.clone();
-                    cx.build_view(|cx| DraggedChannelView { channel, width })
-                }
+            .flex()
+            .w_full()
+            .on_drag(channel.clone(), move |channel, cx| {
+                cx.build_view(|cx| DraggedChannelView {
+                    channel: channel.clone(),
+                    width,
+                })
             })
             .drag_over::<DraggedChannelView>(|style| {
                 style.bg(cx.theme().colors().ghost_element_hover)
@@ -2566,71 +2587,10 @@ impl CollabPanel {
             )
             .child(
                 ListItem::new(channel_id as usize)
-                    .indent_level(depth)
+                    // Offset the indent depth by one to give us room to show the disclosure.
+                    .indent_level(depth + 1)
                     .indent_step_size(cx.rem_size() * 14.0 / 16.0) // @todo()! @nate this is to  step over the disclosure toggle
-                    .start_slot(
-                        IconElement::new(if is_public { Icon::Public } else { Icon::Hash })
-                            .size(IconSize::Small)
-                            .color(Color::Muted),
-                    )
                     .selected(is_selected || is_active)
-                    .child(
-                        h_stack()
-                            .w_full()
-                            .justify_between()
-                            .child(
-                                h_stack()
-                                    .id(channel_id as usize)
-                                    .child(Label::new(channel.name.clone()))
-                                    .children(face_pile.map(|face_pile| face_pile.render(cx))),
-                            )
-                            .child(
-                                h_stack()
-                                    .child(
-                                        div()
-                                            .id("channel_chat")
-                                            .when(!has_messages_notification, |el| el.invisible())
-                                            .group_hover("", |style| style.visible())
-                                            .child(
-                                                IconButton::new(
-                                                    "channel_chat",
-                                                    Icon::MessageBubbles,
-                                                )
-                                                .icon_color(if has_messages_notification {
-                                                    Color::Default
-                                                } else {
-                                                    Color::Muted
-                                                })
-                                                .on_click(cx.listener(move |this, _, cx| {
-                                                    this.join_channel_chat(channel_id, cx)
-                                                }))
-                                                .tooltip(|cx| {
-                                                    Tooltip::text("Open channel chat", cx)
-                                                }),
-                                            ),
-                                    )
-                                    .child(
-                                        div()
-                                            .id("channel_notes")
-                                            .when(!has_notes_notification, |el| el.invisible())
-                                            .group_hover("", |style| style.visible())
-                                            .child(
-                                                IconButton::new("channel_notes", Icon::File)
-                                                    .icon_color(if has_notes_notification {
-                                                        Color::Default
-                                                    } else {
-                                                        Color::Muted
-                                                    })
-                                                    .on_click(cx.listener(move |this, _, cx| {
-                                                        this.open_channel_notes(channel_id, cx)
-                                                    }))
-                                                    .tooltip(|cx| {
-                                                        Tooltip::text("Open channel notes", cx)
-                                                    }),
-                                            ),
-                                    ),
-                            ),
-                    )
                     .toggle(disclosed)
                     .on_toggle(
                         cx.listener(move |this, _, cx| {
@@ -2650,7 +2610,49 @@ impl CollabPanel {
                         move |this, event: &MouseDownEvent, cx| {
                             this.deploy_channel_context_menu(event.position, channel_id, ix, cx)
                         },
-                    )),
+                    ))
+                    .start_slot(
+                        IconElement::new(if is_public { Icon::Public } else { Icon::Hash })
+                            .size(IconSize::Small)
+                            .color(Color::Muted),
+                    )
+                    .child(
+                        h_stack()
+                            .id(channel_id as usize)
+                            .child(Label::new(channel.name.clone()))
+                            .children(face_pile.map(|face_pile| face_pile.render(cx))),
+                    )
+                    .end_slot(
+                        h_stack()
+                            .child(
+                                IconButton::new("channel_chat", Icon::MessageBubbles)
+                                    .icon_color(if has_messages_notification {
+                                        Color::Default
+                                    } else {
+                                        Color::Muted
+                                    })
+                                    .when(!has_messages_notification, |this| {
+                                        this.visible_on_hover("")
+                                    })
+                                    .on_click(cx.listener(move |this, _, cx| {
+                                        this.join_channel_chat(channel_id, cx)
+                                    }))
+                                    .tooltip(|cx| Tooltip::text("Open channel chat", cx)),
+                            )
+                            .child(
+                                IconButton::new("channel_notes", Icon::File)
+                                    .icon_color(if has_notes_notification {
+                                        Color::Default
+                                    } else {
+                                        Color::Muted
+                                    })
+                                    .when(!has_notes_notification, |this| this.visible_on_hover(""))
+                                    .on_click(cx.listener(move |this, _, cx| {
+                                        this.open_channel_notes(channel_id, cx)
+                                    }))
+                                    .tooltip(|cx| Tooltip::text("Open channel notes", cx)),
+                            ),
+                    ),
             )
             .tooltip(|cx| Tooltip::text("Join channel", cx))
 
@@ -3002,7 +3004,7 @@ fn render_tree_branch(is_last: bool, cx: &mut WindowContext) -> impl IntoElement
         let right = bounds.right();
         let top = bounds.top();
 
-        cx.paint_quad(
+        cx.paint_quad(fill(
             Bounds::from_corners(
                 point(start_x, top),
                 point(
@@ -3010,18 +3012,12 @@ fn render_tree_branch(is_last: bool, cx: &mut WindowContext) -> impl IntoElement
                     if is_last { start_y } else { bounds.bottom() },
                 ),
             ),
-            Default::default(),
             color,
-            Default::default(),
-            Hsla::transparent_black(),
-        );
-        cx.paint_quad(
+        ));
+        cx.paint_quad(fill(
             Bounds::from_corners(point(start_x, start_y), point(right, start_y + thickness)),
-            Default::default(),
             color,
-            Default::default(),
-            Hsla::transparent_black(),
-        );
+        ));
     })
     .w(width)
     .h(line_height)

crates/collab_ui2/src/collab_panel/contact_finder.rs πŸ”—

@@ -11,14 +11,8 @@ use ui::{prelude::*, Avatar};
 use util::{ResultExt as _, TryFutureExt};
 use workspace::ModalView;
 
-pub fn init(cx: &mut AppContext) {
-    //Picker::<ContactFinderDelegate>::init(cx);
-    //cx.add_action(ContactFinder::dismiss)
-}
-
 pub struct ContactFinder {
     picker: View<Picker<ContactFinderDelegate>>,
-    has_focus: bool,
 }
 
 impl ContactFinder {
@@ -31,16 +25,12 @@ impl ContactFinder {
         };
         let picker = cx.build_view(|cx| Picker::new(delegate, cx));
 
-        Self {
-            picker,
-            has_focus: false,
-        }
+        Self { picker }
     }
 
     pub fn set_query(&mut self, query: String, cx: &mut ViewContext<Self>) {
         self.picker.update(cx, |picker, cx| {
-            // todo!()
-            // picker.set_query(query, cx);
+            picker.set_query(query, cx);
         });
     }
 }
@@ -62,32 +52,9 @@ impl Render for ContactFinder {
             .w(rems(34.))
     }
 
-    // fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
-    //     self.has_focus = true;
-    //     if cx.is_self_focused() {
-    //         cx.focus(&self.picker)
-    //     }
-    // }
-
-    // fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
-    //     self.has_focus = false;
-    // }
-
     type Element = Div;
 }
 
-// impl Modal for ContactFinder {
-//     fn has_focus(&self) -> bool {
-//         self.has_focus
-//     }
-
-//     fn dismiss_on_event(event: &Self::Event) -> bool {
-//         match event {
-//             PickerEvent::Dismiss => true,
-//         }
-//     }
-// }
-
 pub struct ContactFinderDelegate {
     parent: WeakView<ContactFinder>,
     potential_contacts: Arc<[Arc<User>]>,
@@ -161,7 +128,6 @@ impl PickerDelegate for ContactFinderDelegate {
     }
 
     fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
-        //cx.emit(PickerEvent::Dismiss);
         self.parent
             .update(cx, |_, cx| cx.emit(DismissEvent))
             .log_err();
@@ -191,6 +157,7 @@ impl PickerDelegate for ContactFinderDelegate {
                 .child(Label::new(user.github_login.clone()))
                 .children(icon_path.map(|icon_path| svg().path(icon_path))),
         )
+        // todo!()
         // Flex::row()
         //     .with_children(user.avatar.clone().map(|avatar| {
         //         Image::from_data(avatar)

crates/collab_ui2/src/collab_titlebar_item.rs πŸ”—

@@ -5,8 +5,8 @@ use client::{proto::PeerId, Client, ParticipantIndex, User, UserStore};
 use gpui::{
     actions, canvas, div, overlay, point, px, rems, Action, AnyElement, AppContext, DismissEvent,
     Div, Element, FocusableView, Hsla, InteractiveElement, IntoElement, Model, ParentElement, Path,
-    Render, Stateful, StatefulInteractiveElement, Styled, Subscription, ViewContext, VisualContext,
-    WeakView, WindowBounds,
+    Render, Stateful, StatefulInteractiveElement, Styled, Subscription, View, ViewContext,
+    VisualContext, WeakView, WindowBounds,
 };
 use project::{Project, RepositoryEntry};
 use recent_projects::RecentProjects;
@@ -14,9 +14,10 @@ use std::sync::Arc;
 use theme::{ActiveTheme, PlayerColors};
 use ui::{
     h_stack, popover_menu, prelude::*, Avatar, Button, ButtonLike, ButtonStyle, ContextMenu, Icon,
-    IconButton, IconElement, KeyBinding, Tooltip,
+    IconButton, IconElement, Tooltip,
 };
 use util::ResultExt;
+use vcs_menu::{build_branch_list, BranchList, OpenRecent as ToggleVcsMenu};
 use workspace::{notifications::NotifyResultExt, Workspace, WORKSPACE_DB};
 
 const MAX_PROJECT_NAME_LENGTH: usize = 40;
@@ -51,7 +52,7 @@ pub struct CollabTitlebarItem {
     user_store: Model<UserStore>,
     client: Arc<Client>,
     workspace: WeakView<Workspace>,
-    //branch_popover: Option<ViewHandle<BranchList>>,
+    branch_popover: Option<View<BranchList>>,
     project_popover: Option<recent_projects::RecentProjects>,
     _subscriptions: Vec<Subscription>,
 }
@@ -284,7 +285,7 @@ impl CollabTitlebarItem {
             project,
             user_store,
             client,
-            //         branch_popover: None,
+            branch_popover: None,
             project_popover: None,
             _subscriptions: subscriptions,
         }
@@ -363,23 +364,25 @@ impl CollabTitlebarItem {
             .map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH))?;
 
         Some(
-            div().border().border_color(gpui::red()).child(
-                Button::new("project_branch_trigger", branch_name)
-                    .style(ButtonStyle::Subtle)
-                    .tooltip(move |cx| {
-                        cx.build_view(|_| {
-                            Tooltip::new("Recent Branches")
-                                .key_binding(KeyBinding::new(gpui::KeyBinding::new(
-                                    "cmd-b",
-                                    // todo!() Replace with real action.
-                                    gpui::NoAction,
-                                    None,
-                                )))
-                                .meta("Local branches only")
+            div()
+                .border()
+                .border_color(gpui::red())
+                .child(
+                    Button::new("project_branch_trigger", branch_name)
+                        .style(ButtonStyle::Subtle)
+                        .tooltip(move |cx| {
+                            Tooltip::with_meta(
+                                "Recent Branches",
+                                Some(&ToggleVcsMenu),
+                                "Local branches only",
+                                cx,
+                            )
                         })
-                        .into()
-                    }),
-            ),
+                        .on_click(
+                            cx.listener(|this, _, cx| this.toggle_vcs_menu(&ToggleVcsMenu, cx)),
+                        ),
+                )
+                .children(self.render_branches_popover_host()),
         )
     }
 
@@ -458,103 +461,34 @@ impl CollabTitlebarItem {
             .log_err();
     }
 
-    // fn render_branches_popover_host<'a>(
-    //     &'a self,
-    //     _theme: &'a theme::Titlebar,
-    //     cx: &'a mut ViewContext<Self>,
-    // ) -> Option<AnyElement<Self>> {
-    //     self.branch_popover.as_ref().map(|child| {
-    //         let theme = theme::current(cx).clone();
-    //         let child = ChildView::new(child, cx);
-    //         let child = MouseEventHandler::new::<BranchList, _>(0, cx, |_, _| {
-    //             child
-    //                 .flex(1., true)
-    //                 .contained()
-    //                 .constrained()
-    //                 .with_width(theme.titlebar.menu.width)
-    //                 .with_height(theme.titlebar.menu.height)
-    //         })
-    //         .on_click(MouseButton::Left, |_, _, _| {})
-    //         .on_down_out(MouseButton::Left, move |_, this, cx| {
-    //             this.branch_popover.take();
-    //             cx.emit(());
-    //             cx.notify();
-    //         })
-    //         .contained()
-    //         .into_any();
-
-    //         Overlay::new(child)
-    //             .with_fit_mode(OverlayFitMode::SwitchAnchor)
-    //             .with_anchor_corner(AnchorCorner::TopLeft)
-    //             .with_z_index(999)
-    //             .aligned()
-    //             .bottom()
-    //             .left()
-    //             .into_any()
-    //     })
-    // }
-
-    // fn render_project_popover_host<'a>(
-    //     &'a self,
-    //     _theme: &'a theme::Titlebar,
-    //     cx: &'a mut ViewContext<Self>,
-    // ) -> Option<AnyElement<Self>> {
-    //     self.project_popover.as_ref().map(|child| {
-    //         let theme = theme::current(cx).clone();
-    //         let child = ChildView::new(child, cx);
-    //         let child = MouseEventHandler::new::<RecentProjects, _>(0, cx, |_, _| {
-    //             child
-    //                 .flex(1., true)
-    //                 .contained()
-    //                 .constrained()
-    //                 .with_width(theme.titlebar.menu.width)
-    //                 .with_height(theme.titlebar.menu.height)
-    //         })
-    //         .on_click(MouseButton::Left, |_, _, _| {})
-    //         .on_down_out(MouseButton::Left, move |_, this, cx| {
-    //             this.project_popover.take();
-    //             cx.emit(());
-    //             cx.notify();
-    //         })
-    //         .into_any();
-
-    //         Overlay::new(child)
-    //             .with_fit_mode(OverlayFitMode::SwitchAnchor)
-    //             .with_anchor_corner(AnchorCorner::TopLeft)
-    //             .with_z_index(999)
-    //             .aligned()
-    //             .bottom()
-    //             .left()
-    //             .into_any()
-    //     })
-    // }
-
-    // pub fn toggle_vcs_menu(&mut self, _: &ToggleVcsMenu, cx: &mut ViewContext<Self>) {
-    //     if self.branch_popover.take().is_none() {
-    //         if let Some(workspace) = self.workspace.upgrade(cx) {
-    //             let Some(view) =
-    //                 cx.add_option_view(|cx| build_branch_list(workspace, cx).log_err())
-    //             else {
-    //                 return;
-    //             };
-    //             cx.subscribe(&view, |this, _, event, cx| {
-    //                 match event {
-    //                     PickerEvent::Dismiss => {
-    //                         this.branch_popover = None;
-    //                     }
-    //                 }
-
-    //                 cx.notify();
-    //             })
-    //             .detach();
-    //             self.project_popover.take();
-    //             cx.focus(&view);
-    //             self.branch_popover = Some(view);
-    //         }
-    //     }
-
-    //     cx.notify();
-    // }
+    fn render_branches_popover_host<'a>(&'a self) -> Option<AnyElement> {
+        self.branch_popover.as_ref().map(|child| {
+            overlay()
+                .child(div().min_w_64().child(child.clone()))
+                .into_any()
+        })
+    }
+
+    pub fn toggle_vcs_menu(&mut self, _: &ToggleVcsMenu, cx: &mut ViewContext<Self>) {
+        if self.branch_popover.take().is_none() {
+            if let Some(workspace) = self.workspace.upgrade() {
+                let Some(view) = build_branch_list(workspace, cx).log_err() else {
+                    return;
+                };
+                cx.subscribe(&view, |this, _, _, cx| {
+                    this.branch_popover = None;
+                    cx.notify();
+                })
+                .detach();
+                self.project_popover.take();
+                let focus_handle = view.focus_handle(cx);
+                cx.focus(&focus_handle);
+                self.branch_popover = Some(view);
+            }
+        }
+
+        cx.notify();
+    }
 
     pub fn toggle_project_menu(&mut self, _: &ToggleProjectMenu, cx: &mut ViewContext<Self>) {
         let workspace = self.workspace.clone();

crates/collab_ui2/src/collab_ui.rs πŸ”—

@@ -34,7 +34,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
     ChatPanelSettings::register(cx);
     NotificationPanelSettings::register(cx);
 
-    // vcs_menu::init(cx);
+    vcs_menu::init(cx);
     collab_titlebar_item::init(cx);
     collab_panel::init(cx);
     channel_view::init(cx);

crates/collab_ui2/src/face_pile.rs πŸ”—

@@ -17,7 +17,7 @@ impl RenderOnce for FacePile {
             let isnt_last = ix < player_count - 1;
 
             div()
-                .z_index((player_count - ix) as u32)
+                .z_index((player_count - ix) as u8)
                 .when(isnt_last, |div| div.neg_mr_1())
                 .child(player)
         });

crates/editor2/src/editor.rs πŸ”—

@@ -9297,7 +9297,7 @@ impl Render for Editor {
         let settings = ThemeSettings::get_global(cx);
         let text_style = match self.mode {
             EditorMode::SingleLine | EditorMode::AutoHeight { .. } => TextStyle {
-                color: cx.theme().colors().text,
+                color: cx.theme().colors().editor_foreground,
                 font_family: settings.ui_font.family.clone(),
                 font_features: settings.ui_font.features,
                 font_size: rems(0.875).into(),
@@ -9310,7 +9310,7 @@ impl Render for Editor {
             },
 
             EditorMode::Full => TextStyle {
-                color: cx.theme().colors().text,
+                color: cx.theme().colors().editor_foreground,
                 font_family: settings.buffer_font.family.clone(),
                 font_features: settings.buffer_font.features,
                 font_size: settings.buffer_font_size(cx).into(),
@@ -9763,19 +9763,15 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend
                     .px_1p5()
                     .child(HighlightedLabel::new(line.clone(), highlights.clone()))
                     .child(
-                        div()
-                            .border()
-                            .border_color(gpui::red())
-                            .invisible()
-                            .group_hover(group_id, |style| style.visible())
-                            .child(
-                                IconButton::new(copy_id.clone(), Icon::Copy)
-                                    .icon_color(Color::Muted)
-                                    .size(ButtonSize::Compact)
-                                    .style(ButtonStyle::Transparent)
-                                    .on_click(cx.listener(move |_, _, cx| write_to_clipboard))
-                                    .tooltip(|cx| Tooltip::text("Copy diagnostic message", cx)),
-                            ),
+                        div().border().border_color(gpui::red()).child(
+                            IconButton::new(copy_id.clone(), Icon::Copy)
+                                .icon_color(Color::Muted)
+                                .size(ButtonSize::Compact)
+                                .style(ButtonStyle::Transparent)
+                                .visible_on_hover(group_id)
+                                .on_click(cx.listener(move |_, _, cx| write_to_clipboard))
+                                .tooltip(|cx| Tooltip::text("Copy diagnostic message", cx)),
+                        ),
                     )
             }))
             .into_any_element()

crates/editor2/src/element.rs πŸ”—

@@ -23,13 +23,14 @@ use anyhow::Result;
 use collections::{BTreeMap, HashMap};
 use git::diff::DiffHunkStatus;
 use gpui::{
-    div, overlay, point, px, relative, size, transparent_black, Action, AnchorCorner, AnyElement,
-    AsyncWindowContext, AvailableSpace, BorrowWindow, Bounds, ContentMask, Corners, CursorStyle,
-    DispatchPhase, Edges, Element, ElementId, ElementInputHandler, Entity, EntityId, Hsla,
-    InteractiveBounds, InteractiveElement, IntoElement, LineLayout, ModifiersChangedEvent,
-    MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, RenderOnce,
-    ScrollWheelEvent, ShapedLine, SharedString, Size, StackingOrder, StatefulInteractiveElement,
-    Style, Styled, TextRun, TextStyle, View, ViewContext, WeakView, WindowContext, WrappedLine,
+    div, fill, outline, overlay, point, px, quad, relative, size, transparent_black, Action,
+    AnchorCorner, AnyElement, AsyncWindowContext, AvailableSpace, BorrowWindow, Bounds,
+    ContentMask, Corners, CursorStyle, DispatchPhase, Edges, Element, ElementId,
+    ElementInputHandler, Entity, EntityId, Hsla, InteractiveBounds, InteractiveElement,
+    IntoElement, LineLayout, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent,
+    MouseUpEvent, ParentElement, Pixels, RenderOnce, ScrollWheelEvent, ShapedLine, SharedString,
+    Size, StackingOrder, StatefulInteractiveElement, Style, Styled, TextRun, TextStyle, View,
+    ViewContext, WeakView, WindowContext, WrappedLine,
 };
 use itertools::Itertools;
 use language::{language_settings::ShowWhitespaceSetting, Language};
@@ -620,20 +621,8 @@ impl EditorElement {
         let scroll_top =
             layout.position_map.snapshot.scroll_position().y * layout.position_map.line_height;
         let gutter_bg = cx.theme().colors().editor_gutter_background;
-        cx.paint_quad(
-            gutter_bounds,
-            Corners::default(),
-            gutter_bg,
-            Edges::default(),
-            transparent_black(),
-        );
-        cx.paint_quad(
-            text_bounds,
-            Corners::default(),
-            self.style.background,
-            Edges::default(),
-            transparent_black(),
-        );
+        cx.paint_quad(fill(gutter_bounds, gutter_bg));
+        cx.paint_quad(fill(text_bounds, self.style.background));
 
         if let EditorMode::Full = layout.mode {
             let mut active_rows = layout.active_rows.iter().peekable();
@@ -657,13 +646,7 @@ impl EditorElement {
                         layout.position_map.line_height * (end_row - start_row + 1) as f32,
                     );
                     let active_line_bg = cx.theme().colors().editor_active_line_background;
-                    cx.paint_quad(
-                        Bounds { origin, size },
-                        Corners::default(),
-                        active_line_bg,
-                        Edges::default(),
-                        transparent_black(),
-                    );
+                    cx.paint_quad(fill(Bounds { origin, size }, active_line_bg));
                 }
             }
 
@@ -679,13 +662,7 @@ impl EditorElement {
                     layout.position_map.line_height * highlighted_rows.len() as f32,
                 );
                 let highlighted_line_bg = cx.theme().colors().editor_highlighted_line_background;
-                cx.paint_quad(
-                    Bounds { origin, size },
-                    Corners::default(),
-                    highlighted_line_bg,
-                    Edges::default(),
-                    transparent_black(),
-                );
+                cx.paint_quad(fill(Bounds { origin, size }, highlighted_line_bg));
             }
 
             let scroll_left =
@@ -706,16 +683,13 @@ impl EditorElement {
                 } else {
                     cx.theme().colors().editor_wrap_guide
                 };
-                cx.paint_quad(
+                cx.paint_quad(fill(
                     Bounds {
                         origin: point(x, text_bounds.origin.y),
                         size: size(px(1.), text_bounds.size.height),
                     },
-                    Corners::default(),
                     color,
-                    Edges::default(),
-                    transparent_black(),
-                );
+                ));
             }
         }
     }
@@ -812,13 +786,13 @@ impl EditorElement {
                     let highlight_origin = bounds.origin + point(-width, start_y);
                     let highlight_size = size(width * 2., end_y - start_y);
                     let highlight_bounds = Bounds::new(highlight_origin, highlight_size);
-                    cx.paint_quad(
+                    cx.paint_quad(quad(
                         highlight_bounds,
                         Corners::all(1. * line_height),
                         gpui::yellow(), // todo!("use the right color")
                         Edges::default(),
                         transparent_black(),
-                    );
+                    ));
 
                     continue;
                 }
@@ -845,13 +819,13 @@ impl EditorElement {
                     let highlight_origin = bounds.origin + point(-width, start_y);
                     let highlight_size = size(width * 2., end_y - start_y);
                     let highlight_bounds = Bounds::new(highlight_origin, highlight_size);
-                    cx.paint_quad(
+                    cx.paint_quad(quad(
                         highlight_bounds,
                         Corners::all(1. * line_height),
                         cx.theme().status().deleted,
                         Edges::default(),
                         transparent_black(),
-                    );
+                    ));
 
                     continue;
                 }
@@ -867,13 +841,13 @@ impl EditorElement {
             let highlight_origin = bounds.origin + point(-width, start_y);
             let highlight_size = size(width * 2., end_y - start_y);
             let highlight_bounds = Bounds::new(highlight_origin, highlight_size);
-            cx.paint_quad(
+            cx.paint_quad(quad(
                 highlight_bounds,
                 Corners::all(0.05 * line_height),
                 color, // todo!("use the right color")
                 Edges::default(),
                 transparent_black(),
-            );
+            ));
         }
     }
 
@@ -959,7 +933,7 @@ impl EditorElement {
                                         cx.stop_propagation();
                                     },
                                 ))
-                                .draw(
+                                .draw_and_update_state(
                                     fold_bounds.origin,
                                     fold_bounds.size,
                                     cx,
@@ -1120,121 +1094,117 @@ impl EditorElement {
                         cursor.paint(content_origin, cx);
                     }
                 });
+            },
+        )
+    }
 
-                cx.with_z_index(1, |cx| {
-                    if let Some((position, mut context_menu)) = layout.context_menu.take() {
-                        let available_space =
-                            size(AvailableSpace::MinContent, AvailableSpace::MinContent);
-                        let context_menu_size = context_menu.measure(available_space, cx);
+    fn paint_overlays(
+        &mut self,
+        text_bounds: Bounds<Pixels>,
+        layout: &mut LayoutState,
+        cx: &mut WindowContext,
+    ) {
+        let content_origin = text_bounds.origin + point(layout.gutter_margin, Pixels::ZERO);
+        let start_row = layout.visible_display_row_range.start;
+        if let Some((position, mut context_menu)) = layout.context_menu.take() {
+            let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent);
+            let context_menu_size = context_menu.measure(available_space, cx);
+
+            let cursor_row_layout =
+                &layout.position_map.line_layouts[(position.row() - start_row) as usize].line;
+            let x = cursor_row_layout.x_for_index(position.column() as usize)
+                - layout.position_map.scroll_position.x;
+            let y = (position.row() + 1) as f32 * layout.position_map.line_height
+                - layout.position_map.scroll_position.y;
+            let mut list_origin = content_origin + point(x, y);
+            let list_width = context_menu_size.width;
+            let list_height = context_menu_size.height;
+
+            // Snap the right edge of the list to the right edge of the window if
+            // its horizontal bounds overflow.
+            if list_origin.x + list_width > cx.viewport_size().width {
+                list_origin.x = (cx.viewport_size().width - list_width).max(Pixels::ZERO);
+            }
 
-                        let cursor_row_layout = &layout.position_map.line_layouts
-                            [(position.row() - start_row) as usize]
-                            .line;
-                        let x = cursor_row_layout.x_for_index(position.column() as usize)
-                            - layout.position_map.scroll_position.x;
-                        let y = (position.row() + 1) as f32 * layout.position_map.line_height
-                            - layout.position_map.scroll_position.y;
-                        let mut list_origin = content_origin + point(x, y);
-                        let list_width = context_menu_size.width;
-                        let list_height = context_menu_size.height;
-
-                        // Snap the right edge of the list to the right edge of the window if
-                        // its horizontal bounds overflow.
-                        if list_origin.x + list_width > cx.viewport_size().width {
-                            list_origin.x =
-                                (cx.viewport_size().width - list_width).max(Pixels::ZERO);
-                        }
+            if list_origin.y + list_height > text_bounds.lower_right().y {
+                list_origin.y -= layout.position_map.line_height + list_height;
+            }
 
-                        if list_origin.y + list_height > text_bounds.lower_right().y {
-                            list_origin.y -= layout.position_map.line_height + list_height;
-                        }
+            cx.break_content_mask(|cx| context_menu.draw(list_origin, available_space, cx));
+        }
 
-                        cx.break_content_mask(|cx| {
-                            context_menu.draw(list_origin, available_space, cx)
-                        });
+        if let Some((position, mut hover_popovers)) = layout.hover_popovers.take() {
+            let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent);
+
+            // This is safe because we check on layout whether the required row is available
+            let hovered_row_layout =
+                &layout.position_map.line_layouts[(position.row() - start_row) as usize].line;
+
+            // Minimum required size: Take the first popover, and add 1.5 times the minimum popover
+            // height. This is the size we will use to decide whether to render popovers above or below
+            // the hovered line.
+            let first_size = hover_popovers[0].measure(available_space, cx);
+            let height_to_reserve =
+                first_size.height + 1.5 * MIN_POPOVER_LINE_HEIGHT * layout.position_map.line_height;
+
+            // Compute Hovered Point
+            let x = hovered_row_layout.x_for_index(position.column() as usize)
+                - layout.position_map.scroll_position.x;
+            let y = position.row() as f32 * layout.position_map.line_height
+                - layout.position_map.scroll_position.y;
+            let hovered_point = content_origin + point(x, y);
+
+            if hovered_point.y - height_to_reserve > Pixels::ZERO {
+                // There is enough space above. Render popovers above the hovered point
+                let mut current_y = hovered_point.y;
+                for mut hover_popover in hover_popovers {
+                    let size = hover_popover.measure(available_space, cx);
+                    let mut popover_origin = point(hovered_point.x, current_y - size.height);
+
+                    let x_out_of_bounds =
+                        text_bounds.upper_right().x - (popover_origin.x + size.width);
+                    if x_out_of_bounds < Pixels::ZERO {
+                        popover_origin.x = popover_origin.x + x_out_of_bounds;
                     }
 
-                    if let Some((position, mut hover_popovers)) = layout.hover_popovers.take() {
-                        let available_space =
-                            size(AvailableSpace::MinContent, AvailableSpace::MinContent);
-
-                        // This is safe because we check on layout whether the required row is available
-                        let hovered_row_layout = &layout.position_map.line_layouts
-                            [(position.row() - start_row) as usize]
-                            .line;
-
-                        // Minimum required size: Take the first popover, and add 1.5 times the minimum popover
-                        // height. This is the size we will use to decide whether to render popovers above or below
-                        // the hovered line.
-                        let first_size = hover_popovers[0].measure(available_space, cx);
-                        let height_to_reserve = first_size.height
-                            + 1.5 * MIN_POPOVER_LINE_HEIGHT * layout.position_map.line_height;
-
-                        // Compute Hovered Point
-                        let x = hovered_row_layout.x_for_index(position.column() as usize)
-                            - layout.position_map.scroll_position.x;
-                        let y = position.row() as f32 * layout.position_map.line_height
-                            - layout.position_map.scroll_position.y;
-                        let hovered_point = content_origin + point(x, y);
-
-                        if hovered_point.y - height_to_reserve > Pixels::ZERO {
-                            // There is enough space above. Render popovers above the hovered point
-                            let mut current_y = hovered_point.y;
-                            for mut hover_popover in hover_popovers {
-                                let size = hover_popover.measure(available_space, cx);
-                                let mut popover_origin =
-                                    point(hovered_point.x, current_y - size.height);
-
-                                let x_out_of_bounds =
-                                    text_bounds.upper_right().x - (popover_origin.x + size.width);
-                                if x_out_of_bounds < Pixels::ZERO {
-                                    popover_origin.x = popover_origin.x + x_out_of_bounds;
-                                }
-
-                                cx.break_content_mask(|cx| {
-                                    hover_popover.draw(popover_origin, available_space, cx)
-                                });
+                    cx.break_content_mask(|cx| {
+                        hover_popover.draw(popover_origin, available_space, cx)
+                    });
 
-                                current_y = popover_origin.y - HOVER_POPOVER_GAP;
-                            }
-                        } else {
-                            // There is not enough space above. Render popovers below the hovered point
-                            let mut current_y = hovered_point.y + layout.position_map.line_height;
-                            for mut hover_popover in hover_popovers {
-                                let size = hover_popover.measure(available_space, cx);
-                                let mut popover_origin = point(hovered_point.x, current_y);
-
-                                let x_out_of_bounds =
-                                    text_bounds.upper_right().x - (popover_origin.x + size.width);
-                                if x_out_of_bounds < Pixels::ZERO {
-                                    popover_origin.x = popover_origin.x + x_out_of_bounds;
-                                }
+                    current_y = popover_origin.y - HOVER_POPOVER_GAP;
+                }
+            } else {
+                // There is not enough space above. Render popovers below the hovered point
+                let mut current_y = hovered_point.y + layout.position_map.line_height;
+                for mut hover_popover in hover_popovers {
+                    let size = hover_popover.measure(available_space, cx);
+                    let mut popover_origin = point(hovered_point.x, current_y);
+
+                    let x_out_of_bounds =
+                        text_bounds.upper_right().x - (popover_origin.x + size.width);
+                    if x_out_of_bounds < Pixels::ZERO {
+                        popover_origin.x = popover_origin.x + x_out_of_bounds;
+                    }
 
-                                hover_popover.draw(popover_origin, available_space, cx);
+                    hover_popover.draw(popover_origin, available_space, cx);
 
-                                current_y = popover_origin.y + size.height + HOVER_POPOVER_GAP;
-                            }
-                        }
-                    }
+                    current_y = popover_origin.y + size.height + HOVER_POPOVER_GAP;
+                }
+            }
+        }
 
-                    if let Some(mouse_context_menu) =
-                        self.editor.read(cx).mouse_context_menu.as_ref()
-                    {
-                        let element = overlay()
-                            .position(mouse_context_menu.position)
-                            .child(mouse_context_menu.context_menu.clone())
-                            .anchor(AnchorCorner::TopLeft)
-                            .snap_to_window();
-                        element.draw(
-                            gpui::Point::default(),
-                            size(AvailableSpace::MinContent, AvailableSpace::MinContent),
-                            cx,
-                            |_, _| {},
-                        );
-                    }
-                })
-            },
-        )
+        if let Some(mouse_context_menu) = self.editor.read(cx).mouse_context_menu.as_ref() {
+            let element = overlay()
+                .position(mouse_context_menu.position)
+                .child(mouse_context_menu.context_menu.clone())
+                .anchor(AnchorCorner::TopLeft)
+                .snap_to_window();
+            element.into_any().draw(
+                gpui::Point::default(),
+                size(AvailableSpace::MinContent, AvailableSpace::MinContent),
+                cx,
+            );
+        }
     }
 
     fn scrollbar_left(&self, bounds: &Bounds<Pixels>) -> Pixels {
@@ -1278,7 +1248,7 @@ impl EditorElement {
         let thumb_bounds = Bounds::from_corners(point(left, thumb_top), point(right, thumb_bottom));
 
         if layout.show_scrollbars {
-            cx.paint_quad(
+            cx.paint_quad(quad(
                 track_bounds,
                 Corners::default(),
                 cx.theme().colors().scrollbar_track_background,
@@ -1289,7 +1259,7 @@ impl EditorElement {
                     left: px(1.),
                 },
                 cx.theme().colors().scrollbar_track_border,
-            );
+            ));
             let scrollbar_settings = EditorSettings::get_global(cx).scrollbar;
             if layout.is_singleton && scrollbar_settings.selections {
                 let start_anchor = Anchor::min();
@@ -1309,7 +1279,7 @@ impl EditorElement {
                         end_y = start_y + px(1.);
                     }
                     let bounds = Bounds::from_corners(point(left, start_y), point(right, end_y));
-                    cx.paint_quad(
+                    cx.paint_quad(quad(
                         bounds,
                         Corners::default(),
                         cx.theme().status().info,
@@ -1320,7 +1290,7 @@ impl EditorElement {
                             left: px(1.),
                         },
                         cx.theme().colors().scrollbar_thumb_border,
-                    );
+                    ));
                 }
             }
 
@@ -1352,7 +1322,7 @@ impl EditorElement {
                         DiffHunkStatus::Modified => cx.theme().status().modified,
                         DiffHunkStatus::Removed => cx.theme().status().deleted,
                     };
-                    cx.paint_quad(
+                    cx.paint_quad(quad(
                         bounds,
                         Corners::default(),
                         color,
@@ -1363,11 +1333,11 @@ impl EditorElement {
                             left: px(1.),
                         },
                         cx.theme().colors().scrollbar_thumb_border,
-                    );
+                    ));
                 }
             }
 
-            cx.paint_quad(
+            cx.paint_quad(quad(
                 thumb_bounds,
                 Corners::default(),
                 cx.theme().colors().scrollbar_thumb_background,
@@ -1378,7 +1348,7 @@ impl EditorElement {
                     left: px(1.),
                 },
                 cx.theme().colors().scrollbar_thumb_border,
-            );
+            ));
         }
 
         let mouse_position = cx.mouse_position();
@@ -1525,7 +1495,7 @@ impl EditorElement {
         let scroll_left = scroll_position.x * layout.position_map.em_width;
         let scroll_top = scroll_position.y * layout.position_map.line_height;
 
-        for block in layout.blocks.drain(..) {
+        for mut block in layout.blocks.drain(..) {
             let mut origin = bounds.origin
                 + point(
                     Pixels::ZERO,
@@ -2810,7 +2780,7 @@ impl Element for EditorElement {
     }
 
     fn paint(
-        mut self,
+        &mut self,
         bounds: Bounds<gpui::Pixels>,
         element_state: &mut Self::State,
         cx: &mut gpui::WindowContext,
@@ -2833,32 +2803,30 @@ impl Element for EditorElement {
             self.register_actions(cx);
             self.register_key_listeners(cx);
 
-            // We call with_z_index to establish a new stacking context.
-            cx.with_z_index(0, |cx| {
-                cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
-                    // Paint mouse listeners at z-index 0 so any elements we paint on top of the editor
-                    // take precedence.
-                    cx.with_z_index(0, |cx| {
-                        self.paint_mouse_listeners(bounds, gutter_bounds, text_bounds, &layout, cx);
-                    });
-                    let input_handler = ElementInputHandler::new(bounds, self.editor.clone(), cx);
-                    cx.handle_input(&focus_handle, input_handler);
+            cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
+                let input_handler = ElementInputHandler::new(bounds, self.editor.clone(), cx);
+                cx.handle_input(&focus_handle, input_handler);
 
-                    self.paint_background(gutter_bounds, text_bounds, &layout, cx);
-                    if layout.gutter_size.width > Pixels::ZERO {
-                        self.paint_gutter(gutter_bounds, &mut layout, cx);
-                    }
-                    self.paint_text(text_bounds, &mut layout, cx);
+                self.paint_background(gutter_bounds, text_bounds, &layout, cx);
+                if layout.gutter_size.width > Pixels::ZERO {
+                    self.paint_gutter(gutter_bounds, &mut layout, cx);
+                }
+                self.paint_text(text_bounds, &mut layout, cx);
+
+                cx.with_z_index(0, |cx| {
+                    self.paint_mouse_listeners(bounds, gutter_bounds, text_bounds, &layout, cx);
 
                     if !layout.blocks.is_empty() {
-                        cx.with_z_index(1, |cx| {
-                            cx.with_element_id(Some("editor_blocks"), |cx| {
-                                self.paint_blocks(bounds, &mut layout, cx);
-                            });
-                        })
+                        cx.with_element_id(Some("editor_blocks"), |cx| {
+                            self.paint_blocks(bounds, &mut layout, cx);
+                        });
                     }
+                });
+
+                cx.with_z_index(1, |cx| self.paint_scrollbar(bounds, &mut layout, cx));
 
-                    cx.with_z_index(2, |cx| self.paint_scrollbar(bounds, &mut layout, cx));
+                cx.with_z_index(2, |cx| {
+                    self.paint_overlays(text_bounds, &mut layout, cx);
                 });
             });
         })
@@ -3085,23 +3053,13 @@ impl Cursor {
         };
 
         //Draw background or border quad
-        if matches!(self.shape, CursorShape::Hollow) {
-            cx.paint_quad(
-                bounds,
-                Corners::default(),
-                transparent_black(),
-                Edges::all(px(1.)),
-                self.color,
-            );
+        let cursor = if matches!(self.shape, CursorShape::Hollow) {
+            outline(bounds, self.color)
         } else {
-            cx.paint_quad(
-                bounds,
-                Corners::default(),
-                self.color,
-                Edges::default(),
-                transparent_black(),
-            );
-        }
+            fill(bounds, self.color)
+        };
+
+        cx.paint_quad(cursor);
 
         if let Some(block_text) = &self.block_text {
             block_text.paint(self.origin + origin, self.line_height, cx);

crates/feedback2/Cargo.toml πŸ”—

@@ -18,7 +18,6 @@ gpui = { package = "gpui2", path = "../gpui2" }
 language = { package = "language2", path = "../language2" }
 menu = { package = "menu2", path = "../menu2" }
 project = { package = "project2", path = "../project2" }
-regex.workspace = true
 search = { package = "search2", path = "../search2" }
 settings = { package = "settings2", path = "../settings2" }
 theme = { package = "theme2", path = "../theme2" }
@@ -26,16 +25,20 @@ ui = { package = "ui2", path = "../ui2" }
 util = { path = "../util" }
 workspace = { package = "workspace2", path = "../workspace2"}
 
-log.workspace = true
-futures.workspace = true
-anyhow.workspace = true
-smallvec.workspace = true
+bitflags = "2.4.1"
 human_bytes = "0.4.1"
+
+anyhow.workspace = true
+futures.workspace = true
 isahc.workspace = true
 lazy_static.workspace = true
+log.workspace = true
 postage.workspace = true
+regex.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
+smallvec.workspace = true
+smol.workspace = true
 sysinfo.workspace = true
 tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
 urlencoding = "2.1.2"

crates/feedback2/src/feedback_modal.rs πŸ”—

@@ -1,13 +1,14 @@
-use std::{ops::RangeInclusive, sync::Arc};
+use std::{ops::RangeInclusive, sync::Arc, time::Duration};
 
 use anyhow::{anyhow, bail};
+use bitflags::bitflags;
 use client::{Client, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
 use db::kvp::KEY_VALUE_STORE;
 use editor::{Editor, EditorEvent};
 use futures::AsyncReadExt;
 use gpui::{
-    div, rems, serde_json, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView,
-    Model, PromptLevel, Render, Task, View, ViewContext,
+    div, red, rems, serde_json, AppContext, DismissEvent, Div, EventEmitter, FocusHandle,
+    FocusableView, Model, PromptLevel, Render, Task, View, ViewContext,
 };
 use isahc::Request;
 use language::Buffer;
@@ -22,6 +23,7 @@ use crate::{system_specs::SystemSpecs, GiveFeedback, OpenZedCommunityRepo};
 
 // For UI testing purposes
 const SEND_SUCCESS_IN_DEV_MODE: bool = true;
+const SEND_TIME_IN_DEV_MODE: Duration = Duration::from_secs(2);
 
 // Temporary, until tests are in place
 #[cfg(debug_assertions)]
@@ -47,13 +49,32 @@ struct FeedbackRequestBody<'a> {
     token: &'a str,
 }
 
+bitflags! {
+    #[derive(Debug, Clone, PartialEq)]
+    struct InvalidStateFlags: u8 {
+        const EmailAddress = 0b00000001;
+        const CharacterCount = 0b00000010;
+    }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+enum CannotSubmitReason {
+    InvalidState { flags: InvalidStateFlags },
+    AwaitingSubmission,
+}
+
+#[derive(Debug, Clone, PartialEq)]
+enum SubmissionState {
+    CanSubmit,
+    CannotSubmit { reason: CannotSubmitReason },
+}
+
 pub struct FeedbackModal {
     system_specs: SystemSpecs,
     feedback_editor: View<Editor>,
     email_address_editor: View<Editor>,
-    awaiting_submission: bool,
-    user_submitted: bool,
-    discarded: bool,
+    submission_state: Option<SubmissionState>,
+    dismiss_modal: bool,
     character_count: i32,
 }
 
@@ -66,12 +87,7 @@ impl EventEmitter<DismissEvent> for FeedbackModal {}
 
 impl ModalView for FeedbackModal {
     fn on_before_dismiss(&mut self, cx: &mut ViewContext<Self>) -> bool {
-        if self.user_submitted {
-            self.set_user_submitted(false, cx);
-            return true;
-        }
-
-        if self.discarded {
+        if self.dismiss_modal {
             return true;
         }
 
@@ -85,7 +101,7 @@ impl ModalView for FeedbackModal {
         cx.spawn(move |this, mut cx| async move {
             if answer.await.ok() == Some(0) {
                 this.update(&mut cx, |this, cx| {
-                    this.discarded = true;
+                    this.dismiss_modal = true;
                     cx.emit(DismissEvent)
                 })
                 .log_err();
@@ -159,32 +175,27 @@ impl FeedbackModal {
             editor
         });
 
-        cx.subscribe(
-            &feedback_editor,
-            |this, editor, event: &EditorEvent, cx| match event {
-                EditorEvent::Edited => {
-                    this.character_count = editor
-                        .read(cx)
-                        .buffer()
-                        .read(cx)
-                        .as_singleton()
-                        .expect("Feedback editor is never a multi-buffer")
-                        .read(cx)
-                        .len() as i32;
-                    cx.notify();
-                }
-                _ => {}
-            },
-        )
+        cx.subscribe(&feedback_editor, |this, editor, event: &EditorEvent, cx| {
+            if *event == EditorEvent::Edited {
+                this.character_count = editor
+                    .read(cx)
+                    .buffer()
+                    .read(cx)
+                    .as_singleton()
+                    .expect("Feedback editor is never a multi-buffer")
+                    .read(cx)
+                    .len() as i32;
+                cx.notify();
+            }
+        })
         .detach();
 
         Self {
             system_specs: system_specs.clone(),
             feedback_editor,
             email_address_editor,
-            awaiting_submission: false,
-            user_submitted: false,
-            discarded: false,
+            submission_state: None,
+            dismiss_modal: false,
             character_count: 0,
         }
     }
@@ -205,19 +216,24 @@ impl FeedbackModal {
             if answer == Some(0) {
                 match email.clone() {
                     Some(email) => {
-                        let _ = KEY_VALUE_STORE
+                        KEY_VALUE_STORE
                             .write_kvp(DATABASE_KEY_NAME.to_string(), email)
-                            .await;
+                            .await
+                            .ok();
                     }
                     None => {
-                        let _ = KEY_VALUE_STORE
+                        KEY_VALUE_STORE
                             .delete_kvp(DATABASE_KEY_NAME.to_string())
-                            .await;
+                            .await
+                            .ok();
                     }
                 };
 
                 this.update(&mut cx, |this, cx| {
-                    this.set_awaiting_submission(true, cx);
+                    this.submission_state = Some(SubmissionState::CannotSubmit {
+                        reason: CannotSubmitReason::AwaitingSubmission,
+                    });
+                    cx.notify();
                 })
                 .log_err();
 
@@ -227,7 +243,8 @@ impl FeedbackModal {
                 match res {
                     Ok(_) => {
                         this.update(&mut cx, |this, cx| {
-                            this.set_user_submitted(true, cx);
+                            this.dismiss_modal = true;
+                            cx.notify();
                             cx.emit(DismissEvent)
                         })
                         .ok();
@@ -244,7 +261,9 @@ impl FeedbackModal {
                                 prompt.await.ok();
                             })
                             .detach();
-                            this.set_awaiting_submission(false, cx);
+
+                            this.submission_state = Some(SubmissionState::CanSubmit);
+                            cx.notify();
                         })
                         .log_err();
                     }
@@ -256,16 +275,6 @@ impl FeedbackModal {
         Task::ready(Ok(()))
     }
 
-    fn set_awaiting_submission(&mut self, awaiting_submission: bool, cx: &mut ViewContext<Self>) {
-        self.awaiting_submission = awaiting_submission;
-        cx.notify();
-    }
-
-    fn set_user_submitted(&mut self, user_submitted: bool, cx: &mut ViewContext<Self>) {
-        self.user_submitted = user_submitted;
-        cx.notify();
-    }
-
     async fn submit_feedback(
         feedback_text: &str,
         email: Option<String>,
@@ -273,6 +282,8 @@ impl FeedbackModal {
         system_specs: SystemSpecs,
     ) -> anyhow::Result<()> {
         if DEV_MODE {
+            smol::Timer::after(SEND_TIME_IN_DEV_MODE).await;
+
             if SEND_SUCCESS_IN_DEV_MODE {
                 return Ok(());
             } else {
@@ -309,7 +320,67 @@ impl FeedbackModal {
         Ok(())
     }
 
-    // TODO: Escape button calls dismiss
+    fn update_submission_state(&mut self, cx: &mut ViewContext<Self>) {
+        if self.awaiting_submission() {
+            return;
+        }
+
+        let mut invalid_state_flags = InvalidStateFlags::empty();
+
+        let valid_email_address = match self.email_address_editor.read(cx).text_option(cx) {
+            Some(email_address) => Regex::new(EMAIL_REGEX).unwrap().is_match(&email_address),
+            None => true,
+        };
+
+        if !valid_email_address {
+            invalid_state_flags |= InvalidStateFlags::EmailAddress;
+        }
+
+        if !FEEDBACK_CHAR_LIMIT.contains(&self.character_count) {
+            invalid_state_flags |= InvalidStateFlags::CharacterCount;
+        }
+
+        if invalid_state_flags.is_empty() {
+            self.submission_state = Some(SubmissionState::CanSubmit);
+        } else {
+            self.submission_state = Some(SubmissionState::CannotSubmit {
+                reason: CannotSubmitReason::InvalidState {
+                    flags: invalid_state_flags,
+                },
+            });
+        }
+    }
+
+    fn valid_email_address(&self) -> bool {
+        !self.in_invalid_state(InvalidStateFlags::EmailAddress)
+    }
+
+    fn valid_character_count(&self) -> bool {
+        !self.in_invalid_state(InvalidStateFlags::CharacterCount)
+    }
+
+    fn in_invalid_state(&self, flag: InvalidStateFlags) -> bool {
+        match self.submission_state {
+            Some(SubmissionState::CannotSubmit {
+                reason: CannotSubmitReason::InvalidState { ref flags },
+            }) => flags.contains(flag),
+            _ => false,
+        }
+    }
+
+    fn awaiting_submission(&self) -> bool {
+        matches!(
+            self.submission_state,
+            Some(SubmissionState::CannotSubmit {
+                reason: CannotSubmitReason::AwaitingSubmission
+            })
+        )
+    }
+
+    fn can_submit(&self) -> bool {
+        matches!(self.submission_state, Some(SubmissionState::CanSubmit))
+    }
+
     fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
         cx.emit(DismissEvent)
     }
@@ -319,17 +390,9 @@ impl Render for FeedbackModal {
     type Element = Div;
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
-        let valid_email_address = match self.email_address_editor.read(cx).text_option(cx) {
-            Some(email_address) => Regex::new(EMAIL_REGEX).unwrap().is_match(&email_address),
-            None => true,
-        };
+        self.update_submission_state(cx);
 
-        let valid_character_count = FEEDBACK_CHAR_LIMIT.contains(&self.character_count);
-
-        let allow_submission =
-            valid_character_count && valid_email_address && !self.awaiting_submission;
-
-        let submit_button_text = if self.awaiting_submission {
+        let submit_button_text = if self.awaiting_submission() {
             "Submitting..."
         } else {
             "Submit"
@@ -367,7 +430,7 @@ impl Render for FeedbackModal {
                         *FEEDBACK_CHAR_LIMIT.end() - self.character_count
                     )
                 })
-                .color(if valid_character_count {
+                .color(if self.valid_character_count() {
                     Color::Success
                 } else {
                     Color::Error
@@ -391,7 +454,11 @@ impl Render for FeedbackModal {
                             .p_2()
                             .border()
                             .rounded_md()
-                            .border_color(cx.theme().colors().border)
+                            .border_color(if self.valid_email_address() {
+                                cx.theme().colors().border
+                            } else {
+                                red()
+                            })
                             .child(self.email_address_editor.clone()),
                     )
                     .child(
@@ -424,11 +491,9 @@ impl Render for FeedbackModal {
                                             })),
                                     )
                                     .child(
-                                        Button::new("send_feedback", submit_button_text)
+                                        Button::new("submit_feedback", submit_button_text)
                                             .color(Color::Accent)
                                             .style(ButtonStyle::Filled)
-                                            // TODO: Ensure that while submitting, "Sending..." is shown and disable the button
-                                            // TODO: If submit errors: show popup with error, don't close modal, set text back to "Submit", and re-enable button
                                             .on_click(cx.listener(|this, _, cx| {
                                                 this.submit(cx).detach();
                                             }))
@@ -440,7 +505,7 @@ impl Render for FeedbackModal {
                                                     cx,
                                                 )
                                             })
-                                            .when(!allow_submission, |this| this.disabled(true)),
+                                            .when(!self.can_submit(), |this| this.disabled(true)),
                                     ),
                             ),
                     ),
@@ -450,3 +515,42 @@ impl Render for FeedbackModal {
 
 // TODO: Maybe store email address whenever the modal is closed, versus just on submit, so users can remove it if they want without submitting
 // TODO: Testing of various button states, dismissal prompts, etc.
+
+// #[cfg(test)]
+// mod test {
+//     use super::*;
+
+//     #[test]
+//     fn test_invalid_email_addresses() {
+//         let markdown = markdown.await.log_err();
+//         let buffer = project.update(&mut cx, |project, cx| {
+//             project.create_buffer("", markdown, cx)
+//         })??;
+
+//         workspace.update(&mut cx, |workspace, cx| {
+//             let system_specs = SystemSpecs::new(cx);
+
+//             workspace.toggle_modal(cx, move |cx| {
+//                 let feedback_modal = FeedbackModal::new(system_specs, project, buffer, cx);
+
+//                 assert!(!feedback_modal.can_submit());
+//                 assert!(!feedback_modal.valid_email_address(cx));
+//                 assert!(!feedback_modal.valid_character_count());
+
+//                 feedback_modal
+//                     .email_address_editor
+//                     .update(cx, |this, cx| this.set_text("a", cx));
+//                 feedback_modal.set_submission_state(cx);
+
+//                 assert!(!feedback_modal.valid_email_address(cx));
+
+//                 feedback_modal
+//                     .email_address_editor
+//                     .update(cx, |this, cx| this.set_text("a&b.com", cx));
+//                 feedback_modal.set_submission_state(cx);
+
+//                 assert!(feedback_modal.valid_email_address(cx));
+//             });
+//         })?;
+//     }
+// }

crates/gpui2/src/app.rs πŸ”—

@@ -1138,6 +1138,12 @@ impl AppContext {
     pub fn has_active_drag(&self) -> bool {
         self.active_drag.is_some()
     }
+
+    pub fn active_drag<T: 'static>(&self) -> Option<&T> {
+        self.active_drag
+            .as_ref()
+            .and_then(|drag| drag.value.downcast_ref())
+    }
 }
 
 impl Context for AppContext {
@@ -1292,6 +1298,7 @@ impl<G: 'static> DerefMut for GlobalLease<G> {
 /// within the window or by dragging into the app from the underlying platform.
 pub struct AnyDrag {
     pub view: AnyView,
+    pub value: Box<dyn Any>,
     pub cursor_offset: Point<Pixels>,
 }
 

crates/gpui2/src/element.rs πŸ”—

@@ -23,7 +23,7 @@ pub trait IntoElement: Sized {
         self.into_element().into_any()
     }
 
-    fn draw<T, R>(
+    fn draw_and_update_state<T, R>(
         self,
         origin: Point<Pixels>,
         available_space: Size<T>,
@@ -92,7 +92,7 @@ pub trait Element: 'static + IntoElement {
         cx: &mut WindowContext,
     ) -> (LayoutId, Self::State);
 
-    fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext);
+    fn paint(&mut self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext);
 
     fn into_any(self) -> AnyElement {
         AnyElement::new(self)
@@ -150,8 +150,8 @@ impl<C: RenderOnce> Element for Component<C> {
         }
     }
 
-    fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
-        let element = state.rendered_element.take().unwrap();
+    fn paint(&mut self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
+        let mut element = state.rendered_element.take().unwrap();
         if let Some(element_id) = element.element_id() {
             cx.with_element_state(element_id, |element_state, cx| {
                 let mut element_state = element_state.unwrap();
@@ -420,7 +420,7 @@ impl AnyElement {
         self.0.layout(cx)
     }
 
-    pub fn paint(mut self, cx: &mut WindowContext) {
+    pub fn paint(&mut self, cx: &mut WindowContext) {
         self.0.paint(cx)
     }
 
@@ -435,7 +435,7 @@ impl AnyElement {
 
     /// Initializes this element and performs layout in the available space, then paints it at the given origin.
     pub fn draw(
-        mut self,
+        &mut self,
         origin: Point<Pixels>,
         available_space: Size<AvailableSpace>,
         cx: &mut WindowContext,
@@ -465,8 +465,8 @@ impl Element for AnyElement {
         (layout_id, ())
     }
 
-    fn paint(self, _: Bounds<Pixels>, _: &mut Self::State, cx: &mut WindowContext) {
-        self.paint(cx);
+    fn paint(&mut self, _: Bounds<Pixels>, _: &mut Self::State, cx: &mut WindowContext) {
+        self.paint(cx)
     }
 }
 
@@ -482,48 +482,37 @@ impl IntoElement for AnyElement {
     }
 }
 
-// impl<V, E, F> Element for Option<F>
-// where
-//     V: 'static,
-//     E: Element,
-//     F: FnOnce(&mut V, &mut WindowContext<'_, V>) -> E + 'static,
-// {
-//     type State = Option<AnyElement>;
-
-//     fn element_id(&self) -> Option<ElementId> {
-//         None
-//     }
-
-//     fn layout(
-//         &mut self,
-//         _: Option<Self::State>,
-//         cx: &mut WindowContext,
-//     ) -> (LayoutId, Self::State) {
-//         let render = self.take().unwrap();
-//         let mut element = (render)(view_state, cx).into_any();
-//         let layout_id = element.layout(view_state, cx);
-//         (layout_id, Some(element))
-//     }
-
-//     fn paint(
-//         self,
-//         _bounds: Bounds<Pixels>,
-//         rendered_element: &mut Self::State,
-//         cx: &mut WindowContext,
-//     ) {
-//         rendered_element.take().unwrap().paint(view_state, cx);
-//     }
-// }
-
-// impl<V, E, F> RenderOnce for Option<F>
-// where
-//     V: 'static,
-//     E: Element,
-//     F: FnOnce(&mut V, &mut WindowContext) -> E + 'static,
-// {
-//     type Element = Self;
-
-//     fn render(self) -> Self::Element {
-//         self
-//     }
-// }
+/// The empty element, which renders nothing.
+pub type Empty = ();
+
+impl IntoElement for () {
+    type Element = Self;
+
+    fn element_id(&self) -> Option<ElementId> {
+        None
+    }
+
+    fn into_element(self) -> Self::Element {
+        self
+    }
+}
+
+impl Element for () {
+    type State = ();
+
+    fn layout(
+        &mut self,
+        _state: Option<Self::State>,
+        cx: &mut WindowContext,
+    ) -> (LayoutId, Self::State) {
+        (cx.request_layout(&crate::Style::default(), None), ())
+    }
+
+    fn paint(
+        &mut self,
+        _bounds: Bounds<Pixels>,
+        _state: &mut Self::State,
+        _cx: &mut WindowContext,
+    ) {
+    }
+}

crates/gpui2/src/elements/canvas.rs πŸ”—

@@ -2,15 +2,15 @@ use refineable::Refineable as _;
 
 use crate::{Bounds, Element, IntoElement, Pixels, Style, StyleRefinement, Styled, WindowContext};
 
-pub fn canvas(callback: impl 'static + FnOnce(Bounds<Pixels>, &mut WindowContext)) -> Canvas {
+pub fn canvas(callback: impl 'static + FnOnce(&Bounds<Pixels>, &mut WindowContext)) -> Canvas {
     Canvas {
-        paint_callback: Box::new(callback),
+        paint_callback: Some(Box::new(callback)),
         style: StyleRefinement::default(),
     }
 }
 
 pub struct Canvas {
-    paint_callback: Box<dyn FnOnce(Bounds<Pixels>, &mut WindowContext)>,
+    paint_callback: Option<Box<dyn FnOnce(&Bounds<Pixels>, &mut WindowContext)>>,
     style: StyleRefinement,
 }
 
@@ -27,7 +27,7 @@ impl IntoElement for Canvas {
 }
 
 impl Element for Canvas {
-    type State = ();
+    type State = Style;
 
     fn layout(
         &mut self,
@@ -37,11 +37,13 @@ impl Element for Canvas {
         let mut style = Style::default();
         style.refine(&self.style);
         let layout_id = cx.request_layout(&style, []);
-        (layout_id, ())
+        (layout_id, style)
     }
 
-    fn paint(self, bounds: Bounds<Pixels>, _: &mut (), cx: &mut WindowContext) {
-        (self.paint_callback)(bounds, cx)
+    fn paint(&mut self, bounds: Bounds<Pixels>, style: &mut Style, cx: &mut WindowContext) {
+        style.paint(bounds, cx, |cx| {
+            (self.paint_callback.take().unwrap())(&bounds, cx)
+        });
     }
 }
 

crates/gpui2/src/elements/div.rs πŸ”—

@@ -6,6 +6,7 @@ use crate::{
     SharedString, Size, StackingOrder, Style, StyleRefinement, Styled, Task, View, Visibility,
     WindowContext,
 };
+
 use collections::HashMap;
 use refineable::Refineable;
 use smallvec::SmallVec;
@@ -14,6 +15,7 @@ use std::{
     cell::RefCell,
     cmp::Ordering,
     fmt::Debug,
+    marker::PhantomData,
     mem,
     rc::Rc,
     time::Duration,
@@ -29,6 +31,301 @@ pub struct GroupStyle {
     pub style: Box<StyleRefinement>,
 }
 
+pub struct DragMoveEvent<T> {
+    pub event: MouseMoveEvent,
+    pub bounds: Bounds<Pixels>,
+    drag: PhantomData<T>,
+}
+
+impl<T: 'static> DragMoveEvent<T> {
+    pub fn drag<'b>(&self, cx: &'b AppContext) -> &'b T {
+        cx.active_drag
+            .as_ref()
+            .and_then(|drag| drag.value.downcast_ref::<T>())
+            .expect("DragMoveEvent is only valid when the stored active drag is of the same type.")
+    }
+}
+
+impl Interactivity {
+    pub fn on_mouse_down(
+        &mut self,
+        button: MouseButton,
+        listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
+    ) {
+        self.mouse_down_listeners
+            .push(Box::new(move |event, bounds, phase, cx| {
+                if phase == DispatchPhase::Bubble
+                    && event.button == button
+                    && bounds.visibly_contains(&event.position, cx)
+                {
+                    (listener)(event, cx)
+                }
+            }));
+    }
+
+    pub fn on_any_mouse_down(
+        &mut self,
+        listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
+    ) {
+        self.mouse_down_listeners
+            .push(Box::new(move |event, bounds, phase, cx| {
+                if phase == DispatchPhase::Bubble && bounds.visibly_contains(&event.position, cx) {
+                    (listener)(event, cx)
+                }
+            }));
+    }
+
+    pub fn on_mouse_up(
+        &mut self,
+        button: MouseButton,
+        listener: impl Fn(&MouseUpEvent, &mut WindowContext) + 'static,
+    ) {
+        self.mouse_up_listeners
+            .push(Box::new(move |event, bounds, phase, cx| {
+                if phase == DispatchPhase::Bubble
+                    && event.button == button
+                    && bounds.visibly_contains(&event.position, cx)
+                {
+                    (listener)(event, cx)
+                }
+            }));
+    }
+
+    pub fn on_any_mouse_up(
+        &mut self,
+        listener: impl Fn(&MouseUpEvent, &mut WindowContext) + 'static,
+    ) {
+        self.mouse_up_listeners
+            .push(Box::new(move |event, bounds, phase, cx| {
+                if phase == DispatchPhase::Bubble && bounds.visibly_contains(&event.position, cx) {
+                    (listener)(event, cx)
+                }
+            }));
+    }
+
+    pub fn on_mouse_down_out(
+        &mut self,
+        listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
+    ) {
+        self.mouse_down_listeners
+            .push(Box::new(move |event, bounds, phase, cx| {
+                if phase == DispatchPhase::Capture && !bounds.visibly_contains(&event.position, cx)
+                {
+                    (listener)(event, cx)
+                }
+            }));
+    }
+
+    pub fn on_mouse_up_out(
+        &mut self,
+        button: MouseButton,
+        listener: impl Fn(&MouseUpEvent, &mut WindowContext) + 'static,
+    ) {
+        self.mouse_up_listeners
+            .push(Box::new(move |event, bounds, phase, cx| {
+                if phase == DispatchPhase::Capture
+                    && event.button == button
+                    && !bounds.visibly_contains(&event.position, cx)
+                {
+                    (listener)(event, cx);
+                }
+            }));
+    }
+
+    pub fn on_mouse_move(
+        &mut self,
+        listener: impl Fn(&MouseMoveEvent, &mut WindowContext) + 'static,
+    ) {
+        self.mouse_move_listeners
+            .push(Box::new(move |event, bounds, phase, cx| {
+                if phase == DispatchPhase::Bubble && bounds.visibly_contains(&event.position, cx) {
+                    (listener)(event, cx);
+                }
+            }));
+    }
+
+    pub fn on_drag_move<T>(
+        &mut self,
+        listener: impl Fn(&DragMoveEvent<T>, &mut WindowContext) + 'static,
+    ) where
+        T: 'static,
+    {
+        self.mouse_move_listeners
+            .push(Box::new(move |event, bounds, phase, cx| {
+                if phase == DispatchPhase::Capture
+                    && bounds.drag_target_contains(&event.position, cx)
+                {
+                    if cx
+                        .active_drag
+                        .as_ref()
+                        .is_some_and(|drag| drag.value.as_ref().type_id() == TypeId::of::<T>())
+                    {
+                        (listener)(
+                            &DragMoveEvent {
+                                event: event.clone(),
+                                bounds: bounds.bounds,
+                                drag: PhantomData,
+                            },
+                            cx,
+                        );
+                    }
+                }
+            }));
+    }
+
+    pub fn on_scroll_wheel(
+        &mut self,
+        listener: impl Fn(&ScrollWheelEvent, &mut WindowContext) + 'static,
+    ) {
+        self.scroll_wheel_listeners
+            .push(Box::new(move |event, bounds, phase, cx| {
+                if phase == DispatchPhase::Bubble && bounds.visibly_contains(&event.position, cx) {
+                    (listener)(event, cx);
+                }
+            }));
+    }
+
+    pub fn capture_action<A: Action>(
+        &mut self,
+        listener: impl Fn(&A, &mut WindowContext) + 'static,
+    ) {
+        self.action_listeners.push((
+            TypeId::of::<A>(),
+            Box::new(move |action, phase, cx| {
+                let action = action.downcast_ref().unwrap();
+                if phase == DispatchPhase::Capture {
+                    (listener)(action, cx)
+                }
+            }),
+        ));
+    }
+
+    pub fn on_action<A: Action>(&mut self, listener: impl Fn(&A, &mut WindowContext) + 'static) {
+        self.action_listeners.push((
+            TypeId::of::<A>(),
+            Box::new(move |action, phase, cx| {
+                let action = action.downcast_ref().unwrap();
+                if phase == DispatchPhase::Bubble {
+                    (listener)(action, cx)
+                }
+            }),
+        ));
+    }
+
+    pub fn on_boxed_action(
+        &mut self,
+        action: &Box<dyn Action>,
+        listener: impl Fn(&Box<dyn Action>, &mut WindowContext) + 'static,
+    ) {
+        let action = action.boxed_clone();
+        self.action_listeners.push((
+            (*action).type_id(),
+            Box::new(move |_, phase, cx| {
+                if phase == DispatchPhase::Bubble {
+                    (listener)(&action, cx)
+                }
+            }),
+        ));
+    }
+
+    pub fn on_key_down(&mut self, listener: impl Fn(&KeyDownEvent, &mut WindowContext) + 'static) {
+        self.key_down_listeners
+            .push(Box::new(move |event, phase, cx| {
+                if phase == DispatchPhase::Bubble {
+                    (listener)(event, cx)
+                }
+            }));
+    }
+
+    pub fn capture_key_down(
+        &mut self,
+        listener: impl Fn(&KeyDownEvent, &mut WindowContext) + 'static,
+    ) {
+        self.key_down_listeners
+            .push(Box::new(move |event, phase, cx| {
+                if phase == DispatchPhase::Capture {
+                    listener(event, cx)
+                }
+            }));
+    }
+
+    pub fn on_key_up(&mut self, listener: impl Fn(&KeyUpEvent, &mut WindowContext) + 'static) {
+        self.key_up_listeners
+            .push(Box::new(move |event, phase, cx| {
+                if phase == DispatchPhase::Bubble {
+                    listener(event, cx)
+                }
+            }));
+    }
+
+    pub fn capture_key_up(&mut self, listener: impl Fn(&KeyUpEvent, &mut WindowContext) + 'static) {
+        self.key_up_listeners
+            .push(Box::new(move |event, phase, cx| {
+                if phase == DispatchPhase::Capture {
+                    listener(event, cx)
+                }
+            }));
+    }
+
+    pub fn on_drop<T: 'static>(&mut self, listener: impl Fn(&T, &mut WindowContext) + 'static) {
+        self.drop_listeners.push((
+            TypeId::of::<T>(),
+            Box::new(move |dragged_value, cx| {
+                listener(dragged_value.downcast_ref().unwrap(), cx);
+            }),
+        ));
+    }
+
+    pub fn on_click(&mut self, listener: impl Fn(&ClickEvent, &mut WindowContext) + 'static)
+    where
+        Self: Sized,
+    {
+        self.click_listeners
+            .push(Box::new(move |event, cx| listener(event, cx)));
+    }
+
+    pub fn on_drag<T, W>(
+        &mut self,
+        value: T,
+        constructor: impl Fn(&T, &mut WindowContext) -> View<W> + 'static,
+    ) where
+        Self: Sized,
+        T: 'static,
+        W: 'static + Render,
+    {
+        debug_assert!(
+            self.drag_listener.is_none(),
+            "calling on_drag more than once on the same element is not supported"
+        );
+        self.drag_listener = Some((
+            Box::new(value),
+            Box::new(move |value, cx| constructor(value.downcast_ref().unwrap(), cx).into()),
+        ));
+    }
+
+    pub fn on_hover(&mut self, listener: impl Fn(&bool, &mut WindowContext) + 'static)
+    where
+        Self: Sized,
+    {
+        debug_assert!(
+            self.hover_listener.is_none(),
+            "calling on_hover more than once on the same element is not supported"
+        );
+        self.hover_listener = Some(Box::new(listener));
+    }
+
+    pub fn tooltip(&mut self, build_tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static)
+    where
+        Self: Sized,
+    {
+        debug_assert!(
+            self.tooltip_builder.is_none(),
+            "calling tooltip more than once on the same element is not supported"
+        );
+        self.tooltip_builder = Some(Rc::new(build_tooltip));
+    }
+}
+
 pub trait InteractiveElement: Sized {
     fn interactivity(&mut self) -> &mut Interactivity;
 
@@ -86,16 +383,7 @@ pub trait InteractiveElement: Sized {
         button: MouseButton,
         listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
     ) -> Self {
-        self.interactivity().mouse_down_listeners.push(Box::new(
-            move |event, bounds, phase, cx| {
-                if phase == DispatchPhase::Bubble
-                    && event.button == button
-                    && bounds.visibly_contains(&event.position, cx)
-                {
-                    (listener)(event, cx)
-                }
-            },
-        ));
+        self.interactivity().on_mouse_down(button, listener);
         self
     }
 
@@ -103,13 +391,7 @@ pub trait InteractiveElement: Sized {
         mut self,
         listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
     ) -> Self {
-        self.interactivity().mouse_down_listeners.push(Box::new(
-            move |event, bounds, phase, cx| {
-                if phase == DispatchPhase::Bubble && bounds.visibly_contains(&event.position, cx) {
-                    (listener)(event, cx)
-                }
-            },
-        ));
+        self.interactivity().on_any_mouse_down(listener);
         self
     }
 
@@ -118,30 +400,7 @@ pub trait InteractiveElement: Sized {
         button: MouseButton,
         listener: impl Fn(&MouseUpEvent, &mut WindowContext) + 'static,
     ) -> Self {
-        self.interactivity()
-            .mouse_up_listeners
-            .push(Box::new(move |event, bounds, phase, cx| {
-                if phase == DispatchPhase::Bubble
-                    && event.button == button
-                    && bounds.visibly_contains(&event.position, cx)
-                {
-                    (listener)(event, cx)
-                }
-            }));
-        self
-    }
-
-    fn on_any_mouse_up(
-        mut self,
-        listener: impl Fn(&MouseUpEvent, &mut WindowContext) + 'static,
-    ) -> Self {
-        self.interactivity()
-            .mouse_up_listeners
-            .push(Box::new(move |event, bounds, phase, cx| {
-                if phase == DispatchPhase::Bubble && bounds.visibly_contains(&event.position, cx) {
-                    (listener)(event, cx)
-                }
-            }));
+        self.interactivity().on_mouse_up(button, listener);
         self
     }
 
@@ -149,14 +408,7 @@ pub trait InteractiveElement: Sized {
         mut self,
         listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
     ) -> Self {
-        self.interactivity().mouse_down_listeners.push(Box::new(
-            move |event, bounds, phase, cx| {
-                if phase == DispatchPhase::Capture && !bounds.visibly_contains(&event.position, cx)
-                {
-                    (listener)(event, cx)
-                }
-            },
-        ));
+        self.interactivity().on_mouse_down_out(listener);
         self
     }
 
@@ -165,16 +417,7 @@ pub trait InteractiveElement: Sized {
         button: MouseButton,
         listener: impl Fn(&MouseUpEvent, &mut WindowContext) + 'static,
     ) -> Self {
-        self.interactivity()
-            .mouse_up_listeners
-            .push(Box::new(move |event, bounds, phase, cx| {
-                if phase == DispatchPhase::Capture
-                    && event.button == button
-                    && !bounds.visibly_contains(&event.position, cx)
-                {
-                    (listener)(event, cx);
-                }
-            }));
+        self.interactivity().on_mouse_up_out(button, listener);
         self
     }
 
@@ -182,13 +425,18 @@ pub trait InteractiveElement: Sized {
         mut self,
         listener: impl Fn(&MouseMoveEvent, &mut WindowContext) + 'static,
     ) -> Self {
-        self.interactivity().mouse_move_listeners.push(Box::new(
-            move |event, bounds, phase, cx| {
-                if phase == DispatchPhase::Bubble && bounds.visibly_contains(&event.position, cx) {
-                    (listener)(event, cx);
-                }
-            },
-        ));
+        self.interactivity().on_mouse_move(listener);
+        self
+    }
+
+    fn on_drag_move<T: 'static>(
+        mut self,
+        listener: impl Fn(&DragMoveEvent<T>, &mut WindowContext) + 'static,
+    ) -> Self
+    where
+        T: 'static,
+    {
+        self.interactivity().on_drag_move(listener);
         self
     }
 
@@ -196,13 +444,7 @@ pub trait InteractiveElement: Sized {
         mut self,
         listener: impl Fn(&ScrollWheelEvent, &mut WindowContext) + 'static,
     ) -> Self {
-        self.interactivity().scroll_wheel_listeners.push(Box::new(
-            move |event, bounds, phase, cx| {
-                if phase == DispatchPhase::Bubble && bounds.visibly_contains(&event.position, cx) {
-                    (listener)(event, cx);
-                }
-            },
-        ));
+        self.interactivity().on_scroll_wheel(listener);
         self
     }
 
@@ -211,29 +453,13 @@ pub trait InteractiveElement: Sized {
         mut self,
         listener: impl Fn(&A, &mut WindowContext) + 'static,
     ) -> Self {
-        self.interactivity().action_listeners.push((
-            TypeId::of::<A>(),
-            Box::new(move |action, phase, cx| {
-                let action = action.downcast_ref().unwrap();
-                if phase == DispatchPhase::Capture {
-                    (listener)(action, cx)
-                }
-            }),
-        ));
+        self.interactivity().capture_action(listener);
         self
     }
 
     /// Add a listener for the given action, fires during the bubble event phase
     fn on_action<A: Action>(mut self, listener: impl Fn(&A, &mut WindowContext) + 'static) -> Self {
-        self.interactivity().action_listeners.push((
-            TypeId::of::<A>(),
-            Box::new(move |action, phase, cx| {
-                let action = action.downcast_ref().unwrap();
-                if phase == DispatchPhase::Bubble {
-                    (listener)(action, cx)
-                }
-            }),
-        ));
+        self.interactivity().on_action(listener);
         self
     }
 
@@ -242,15 +468,7 @@ pub trait InteractiveElement: Sized {
         action: &Box<dyn Action>,
         listener: impl Fn(&Box<dyn Action>, &mut WindowContext) + 'static,
     ) -> Self {
-        let action = action.boxed_clone();
-        self.interactivity().action_listeners.push((
-            (*action).type_id(),
-            Box::new(move |_, phase, cx| {
-                if phase == DispatchPhase::Bubble {
-                    (listener)(&action, cx)
-                }
-            }),
-        ));
+        self.interactivity().on_boxed_action(action, listener);
         self
     }
 
@@ -258,13 +476,7 @@ pub trait InteractiveElement: Sized {
         mut self,
         listener: impl Fn(&KeyDownEvent, &mut WindowContext) + 'static,
     ) -> Self {
-        self.interactivity()
-            .key_down_listeners
-            .push(Box::new(move |event, phase, cx| {
-                if phase == DispatchPhase::Bubble {
-                    (listener)(event, cx)
-                }
-            }));
+        self.interactivity().on_key_down(listener);
         self
     }
 
@@ -272,24 +484,12 @@ pub trait InteractiveElement: Sized {
         mut self,
         listener: impl Fn(&KeyDownEvent, &mut WindowContext) + 'static,
     ) -> Self {
-        self.interactivity()
-            .key_down_listeners
-            .push(Box::new(move |event, phase, cx| {
-                if phase == DispatchPhase::Capture {
-                    listener(event, cx)
-                }
-            }));
+        self.interactivity().capture_key_down(listener);
         self
     }
 
     fn on_key_up(mut self, listener: impl Fn(&KeyUpEvent, &mut WindowContext) + 'static) -> Self {
-        self.interactivity()
-            .key_up_listeners
-            .push(Box::new(move |event, phase, cx| {
-                if phase == DispatchPhase::Bubble {
-                    listener(event, cx)
-                }
-            }));
+        self.interactivity().on_key_up(listener);
         self
     }
 
@@ -297,13 +497,7 @@ pub trait InteractiveElement: Sized {
         mut self,
         listener: impl Fn(&KeyUpEvent, &mut WindowContext) + 'static,
     ) -> Self {
-        self.interactivity()
-            .key_up_listeners
-            .push(Box::new(move |event, phase, cx| {
-                if phase == DispatchPhase::Capture {
-                    listener(event, cx)
-                }
-            }));
+        self.interactivity().capture_key_up(listener);
         self
     }
 
@@ -329,16 +523,8 @@ pub trait InteractiveElement: Sized {
         self
     }
 
-    fn on_drop<W: 'static>(
-        mut self,
-        listener: impl Fn(&View<W>, &mut WindowContext) + 'static,
-    ) -> Self {
-        self.interactivity().drop_listeners.push((
-            TypeId::of::<W>(),
-            Box::new(move |dragged_view, cx| {
-                listener(&dragged_view.downcast().unwrap(), cx);
-            }),
-        ));
+    fn on_drop<T: 'static>(mut self, listener: impl Fn(&T, &mut WindowContext) + 'static) -> Self {
+        self.interactivity().on_drop(listener);
         self
     }
 }
@@ -397,25 +583,21 @@ pub trait StatefulInteractiveElement: InteractiveElement {
     where
         Self: Sized,
     {
-        self.interactivity()
-            .click_listeners
-            .push(Box::new(move |event, cx| listener(event, cx)));
+        self.interactivity().on_click(listener);
         self
     }
 
-    fn on_drag<W>(mut self, listener: impl Fn(&mut WindowContext) -> View<W> + 'static) -> Self
+    fn on_drag<T, W>(
+        mut self,
+        value: T,
+        constructor: impl Fn(&T, &mut WindowContext) -> View<W> + 'static,
+    ) -> Self
     where
         Self: Sized,
+        T: 'static,
         W: 'static + Render,
     {
-        debug_assert!(
-            self.interactivity().drag_listener.is_none(),
-            "calling on_drag more than once on the same element is not supported"
-        );
-        self.interactivity().drag_listener = Some(Box::new(move |cursor_offset, cx| AnyDrag {
-            view: listener(cx).into(),
-            cursor_offset,
-        }));
+        self.interactivity().on_drag(value, constructor);
         self
     }
 
@@ -423,11 +605,7 @@ pub trait StatefulInteractiveElement: InteractiveElement {
     where
         Self: Sized,
     {
-        debug_assert!(
-            self.interactivity().hover_listener.is_none(),
-            "calling on_hover more than once on the same element is not supported"
-        );
-        self.interactivity().hover_listener = Some(Box::new(listener));
+        self.interactivity().on_hover(listener);
         self
     }
 
@@ -435,11 +613,7 @@ pub trait StatefulInteractiveElement: InteractiveElement {
     where
         Self: Sized,
     {
-        debug_assert!(
-            self.interactivity().tooltip_builder.is_none(),
-            "calling tooltip more than once on the same element is not supported"
-        );
-        self.interactivity().tooltip_builder = Some(Rc::new(build_tooltip));
+        self.interactivity().tooltip(build_tooltip);
         self
     }
 }
@@ -479,9 +653,9 @@ pub type ScrollWheelListener =
 
 pub type ClickListener = Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>;
 
-pub type DragListener = Box<dyn Fn(Point<Pixels>, &mut WindowContext) -> AnyDrag + 'static>;
+pub type DragListener = Box<dyn Fn(&dyn Any, &mut WindowContext) -> AnyView + 'static>;
 
-type DropListener = dyn Fn(AnyView, &mut WindowContext) + 'static;
+type DropListener = Box<dyn Fn(&dyn Any, &mut WindowContext) + 'static>;
 
 pub type TooltipBuilder = Rc<dyn Fn(&mut WindowContext) -> AnyView + 'static>;
 
@@ -493,9 +667,20 @@ pub type DragEventListener = Box<dyn Fn(&MouseMoveEvent, &mut WindowContext) + '
 
 pub type ActionListener = Box<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext) + 'static>;
 
+#[track_caller]
 pub fn div() -> Div {
+    #[cfg(debug_assertions)]
+    let interactivity = {
+        let mut interactivity = Interactivity::default();
+        interactivity.location = Some(*core::panic::Location::caller());
+        interactivity
+    };
+
+    #[cfg(not(debug_assertions))]
+    let interactivity = Interactivity::default();
+
     Div {
-        interactivity: Interactivity::default(),
+        interactivity,
         children: SmallVec::default(),
     }
 }
@@ -556,7 +741,7 @@ impl Element for Div {
     }
 
     fn paint(
-        self,
+        &mut self,
         bounds: Bounds<Pixels>,
         element_state: &mut Self::State,
         cx: &mut WindowContext,
@@ -600,27 +785,20 @@ impl Element for Div {
             &mut element_state.interactive_state,
             cx,
             |style, scroll_offset, cx| {
-                if style.visibility == Visibility::Hidden {
-                    return;
-                }
-
                 let z_index = style.z_index.unwrap_or(0);
 
                 cx.with_z_index(z_index, |cx| {
-                    cx.with_z_index(0, |cx| {
-                        style.paint(bounds, cx);
-                    });
-                    cx.with_z_index(1, |cx| {
+                    style.paint(bounds, cx, |cx| {
                         cx.with_text_style(style.text_style().cloned(), |cx| {
                             cx.with_content_mask(style.overflow_mask(bounds), |cx| {
                                 cx.with_element_offset(scroll_offset, |cx| {
-                                    for child in self.children {
+                                    for child in &mut self.children {
                                         child.paint(cx);
                                     }
                                 })
                             })
                         })
-                    })
+                    });
                 })
             },
         );
@@ -646,7 +824,10 @@ pub struct DivState {
 
 impl DivState {
     pub fn is_active(&self) -> bool {
-        self.interactive_state.pending_mouse_down.borrow().is_some()
+        self.interactive_state
+            .pending_mouse_down
+            .as_ref()
+            .map_or(false, |pending| pending.borrow().is_some())
     }
 }
 
@@ -673,11 +854,14 @@ pub struct Interactivity {
     pub key_down_listeners: Vec<KeyDownListener>,
     pub key_up_listeners: Vec<KeyUpListener>,
     pub action_listeners: Vec<(TypeId, ActionListener)>,
-    pub drop_listeners: Vec<(TypeId, Box<DropListener>)>,
+    pub drop_listeners: Vec<(TypeId, DropListener)>,
     pub click_listeners: Vec<ClickListener>,
-    pub drag_listener: Option<DragListener>,
+    pub drag_listener: Option<(Box<dyn Any>, DragListener)>,
     pub hover_listener: Option<Box<dyn Fn(&bool, &mut WindowContext)>>,
     pub tooltip_builder: Option<TooltipBuilder>,
+
+    #[cfg(debug_assertions)]
+    pub location: Option<core::panic::Location<'static>>,
 }
 
 #[derive(Clone, Debug)]
@@ -728,7 +912,7 @@ impl Interactivity {
     }
 
     pub fn paint(
-        mut self,
+        &mut self,
         bounds: Bounds<Pixels>,
         content_size: Size<Pixels>,
         element_state: &mut InteractiveElementState,
@@ -737,6 +921,121 @@ impl Interactivity {
     ) {
         let style = self.compute_style(Some(bounds), element_state, cx);
 
+        if style.visibility == Visibility::Hidden {
+            return;
+        }
+
+        #[cfg(debug_assertions)]
+        if self.element_id.is_some()
+            && (style.debug || style.debug_below || cx.has_global::<crate::DebugBelow>())
+            && bounds.contains(&cx.mouse_position())
+        {
+            const FONT_SIZE: crate::Pixels = crate::Pixels(10.);
+            let element_id = format!("{:?}", self.element_id.as_ref().unwrap());
+            let str_len = element_id.len();
+
+            let render_debug_text = |cx: &mut WindowContext| {
+                if let Some(text) = cx
+                    .text_system()
+                    .shape_text(
+                        &element_id,
+                        FONT_SIZE,
+                        &[cx.text_style().to_run(str_len)],
+                        None,
+                    )
+                    .ok()
+                    .map(|mut text| text.pop())
+                    .flatten()
+                {
+                    text.paint(bounds.origin, FONT_SIZE, cx).ok();
+
+                    let text_bounds = crate::Bounds {
+                        origin: bounds.origin,
+                        size: text.size(FONT_SIZE),
+                    };
+                    if self.location.is_some()
+                        && text_bounds.contains(&cx.mouse_position())
+                        && cx.modifiers().command
+                    {
+                        let command_held = cx.modifiers().command;
+                        cx.on_key_event({
+                            let text_bounds = text_bounds.clone();
+                            move |e: &crate::ModifiersChangedEvent, _phase, cx| {
+                                if e.modifiers.command != command_held
+                                    && text_bounds.contains(&cx.mouse_position())
+                                {
+                                    cx.notify();
+                                }
+                            }
+                        });
+
+                        let hovered = bounds.contains(&cx.mouse_position());
+                        cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
+                            if phase == DispatchPhase::Capture {
+                                if bounds.contains(&event.position) != hovered {
+                                    cx.notify();
+                                }
+                            }
+                        });
+
+                        cx.on_mouse_event({
+                            let location = self.location.clone().unwrap();
+                            let text_bounds = text_bounds.clone();
+                            move |e: &crate::MouseDownEvent, phase, cx| {
+                                if text_bounds.contains(&e.position) && phase.capture() {
+                                    cx.stop_propagation();
+                                    let Ok(dir) = std::env::current_dir() else {
+                                        return;
+                                    };
+
+                                    eprintln!(
+                                        "This element is created at:\n{}:{}:{}",
+                                        location.file(),
+                                        location.line(),
+                                        location.column()
+                                    );
+
+                                    std::process::Command::new("zed")
+                                        .arg(format!(
+                                            "{}/{}:{}:{}",
+                                            dir.to_string_lossy(),
+                                            location.file(),
+                                            location.line(),
+                                            location.column()
+                                        ))
+                                        .spawn()
+                                        .ok();
+                                }
+                            }
+                        });
+                        cx.paint_quad(crate::outline(
+                            crate::Bounds {
+                                origin: bounds.origin
+                                    + crate::point(crate::px(0.), FONT_SIZE - px(2.)),
+                                size: crate::Size {
+                                    width: text_bounds.size.width,
+                                    height: crate::px(1.),
+                                },
+                            },
+                            crate::red(),
+                        ))
+                    }
+                }
+            };
+
+            cx.with_z_index(1, |cx| {
+                cx.with_text_style(
+                    Some(crate::TextStyleRefinement {
+                        color: Some(crate::red()),
+                        line_height: Some(FONT_SIZE.into()),
+                        background_color: Some(crate::white()),
+                        ..Default::default()
+                    }),
+                    render_debug_text,
+                )
+            });
+        }
+
         if style
             .background
             .as_ref()
@@ -745,10 +1044,10 @@ impl Interactivity {
             cx.with_z_index(style.z_index.unwrap_or(0), |cx| cx.add_opaque_layer(bounds))
         }
 
-        let interactive_bounds = Rc::new(InteractiveBounds {
+        let interactive_bounds = InteractiveBounds {
             bounds: bounds.intersect(&cx.content_mask().bounds),
             stacking_order: cx.stacking_order().clone(),
-        });
+        };
 
         if let Some(mouse_cursor) = style.mouse_cursor {
             let mouse_position = &cx.mouse_position();
@@ -781,28 +1080,28 @@ impl Interactivity {
         for listener in self.mouse_down_listeners.drain(..) {
             let interactive_bounds = interactive_bounds.clone();
             cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
-                listener(event, &*interactive_bounds, phase, cx);
+                listener(event, &interactive_bounds, phase, cx);
             })
         }
 
         for listener in self.mouse_up_listeners.drain(..) {
             let interactive_bounds = interactive_bounds.clone();
             cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| {
-                listener(event, &*interactive_bounds, phase, cx);
+                listener(event, &interactive_bounds, phase, cx);
             })
         }
 
         for listener in self.mouse_move_listeners.drain(..) {
             let interactive_bounds = interactive_bounds.clone();
             cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
-                listener(event, &*interactive_bounds, phase, cx);
+                listener(event, &interactive_bounds, phase, cx);
             })
         }
 
         for listener in self.scroll_wheel_listeners.drain(..) {
             let interactive_bounds = interactive_bounds.clone();
             cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| {
-                listener(event, &*interactive_bounds, phase, cx);
+                listener(event, &interactive_bounds, phase, cx);
             })
         }
 
@@ -845,8 +1144,10 @@ impl Interactivity {
                     if phase == DispatchPhase::Bubble
                         && interactive_bounds.drag_target_contains(&event.position, cx)
                     {
-                        if let Some(drag_state_type) =
-                            cx.active_drag.as_ref().map(|drag| drag.view.entity_type())
+                        if let Some(drag_state_type) = cx
+                            .active_drag
+                            .as_ref()
+                            .map(|drag| drag.value.as_ref().type_id())
                         {
                             for (drop_state_type, listener) in &drop_listeners {
                                 if *drop_state_type == drag_state_type {
@@ -855,7 +1156,7 @@ impl Interactivity {
                                         .take()
                                         .expect("checked for type drag state type above");
 
-                                    listener(drag.view.clone(), cx);
+                                    listener(drag.value.as_ref(), cx);
                                     cx.notify();
                                     cx.stop_propagation();
                                 }
@@ -869,14 +1170,20 @@ impl Interactivity {
         }
 
         let click_listeners = mem::take(&mut self.click_listeners);
-        let drag_listener = mem::take(&mut self.drag_listener);
+        let mut drag_listener = mem::take(&mut self.drag_listener);
 
         if !click_listeners.is_empty() || drag_listener.is_some() {
-            let pending_mouse_down = element_state.pending_mouse_down.clone();
+            let pending_mouse_down = element_state
+                .pending_mouse_down
+                .get_or_insert_with(Default::default)
+                .clone();
             let mouse_down = pending_mouse_down.borrow().clone();
             if let Some(mouse_down) = mouse_down {
-                if let Some(drag_listener) = drag_listener {
-                    let active_state = element_state.clicked_state.clone();
+                if drag_listener.is_some() {
+                    let active_state = element_state
+                        .clicked_state
+                        .get_or_insert_with(Default::default)
+                        .clone();
                     let interactive_bounds = interactive_bounds.clone();
 
                     cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
@@ -888,10 +1195,18 @@ impl Interactivity {
                             && interactive_bounds.visibly_contains(&event.position, cx)
                             && (event.position - mouse_down.position).magnitude() > DRAG_THRESHOLD
                         {
+                            let (drag_value, drag_listener) = drag_listener
+                                .take()
+                                .expect("The notify below should invalidate this callback");
+
                             *active_state.borrow_mut() = ElementClickedState::default();
                             let cursor_offset = event.position - bounds.origin;
-                            let drag = drag_listener(cursor_offset, cx);
-                            cx.active_drag = Some(drag);
+                            let drag = (drag_listener)(drag_value.as_ref(), cx);
+                            cx.active_drag = Some(AnyDrag {
+                                view: drag,
+                                value: drag_value,
+                                cursor_offset,
+                            });
                             cx.notify();
                             cx.stop_propagation();
                         }
@@ -929,8 +1244,14 @@ impl Interactivity {
         }
 
         if let Some(hover_listener) = self.hover_listener.take() {
-            let was_hovered = element_state.hover_state.clone();
-            let has_mouse_down = element_state.pending_mouse_down.clone();
+            let was_hovered = element_state
+                .hover_state
+                .get_or_insert_with(Default::default)
+                .clone();
+            let has_mouse_down = element_state
+                .pending_mouse_down
+                .get_or_insert_with(Default::default)
+                .clone();
             let interactive_bounds = interactive_bounds.clone();
 
             cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {

crates/gpui2/src/elements/img.rs πŸ”—

@@ -81,11 +81,12 @@ impl Element for Img {
     }
 
     fn paint(
-        self,
+        &mut self,
         bounds: Bounds<Pixels>,
         element_state: &mut Self::State,
         cx: &mut WindowContext,
     ) {
+        let source = self.source.clone();
         self.interactivity.paint(
             bounds,
             bounds.size,
@@ -94,7 +95,7 @@ impl Element for Img {
             |style, _scroll_offset, cx| {
                 let corner_radii = style.corner_radii.to_pixels(bounds.size, cx.rem_size());
                 cx.with_z_index(1, |cx| {
-                    match self.source {
+                    match source {
                         ImageSource::Uri(uri) => {
                             let image_future = cx.image_cache.get(uri.clone());
                             if let Some(data) = image_future

crates/gpui2/src/elements/list.rs πŸ”—

@@ -257,7 +257,7 @@ impl Element for List {
     }
 
     fn paint(
-        self,
+        &mut self,
         bounds: crate::Bounds<crate::Pixels>,
         _state: &mut Self::State,
         cx: &mut crate::WindowContext,
@@ -385,7 +385,7 @@ impl Element for List {
         // Paint the visible items
         let mut item_origin = bounds.origin;
         item_origin.y -= scroll_top.offset_in_item;
-        for mut item_element in item_elements {
+        for item_element in &mut item_elements {
             let item_height = item_element.measure(available_item_space, cx).height;
             item_element.draw(item_origin, available_item_space, cx);
             item_origin.y += item_height;

crates/gpui2/src/elements/overlay.rs πŸ”—

@@ -81,7 +81,7 @@ impl Element for Overlay {
     }
 
     fn paint(
-        self,
+        &mut self,
         bounds: crate::Bounds<crate::Pixels>,
         element_state: &mut Self::State,
         cx: &mut WindowContext,
@@ -149,7 +149,7 @@ impl Element for Overlay {
 
         cx.with_element_offset(desired.origin - bounds.origin, |cx| {
             cx.break_content_mask(|cx| {
-                for child in self.children {
+                for child in &mut self.children {
                     child.paint(cx);
                 }
             })

crates/gpui2/src/elements/svg.rs πŸ”—

@@ -36,8 +36,12 @@ impl Element for Svg {
         })
     }
 
-    fn paint(self, bounds: Bounds<Pixels>, element_state: &mut Self::State, cx: &mut WindowContext)
-    where
+    fn paint(
+        &mut self,
+        bounds: Bounds<Pixels>,
+        element_state: &mut Self::State,
+        cx: &mut WindowContext,
+    ) where
         Self: Sized,
     {
         self.interactivity

crates/gpui2/src/elements/text.rs πŸ”—

@@ -6,7 +6,7 @@ use crate::{
 use anyhow::anyhow;
 use parking_lot::{Mutex, MutexGuard};
 use smallvec::SmallVec;
-use std::{cell::Cell, ops::Range, rc::Rc, sync::Arc};
+use std::{cell::Cell, mem, ops::Range, rc::Rc, sync::Arc};
 use util::ResultExt;
 
 impl Element for &'static str {
@@ -22,7 +22,7 @@ impl Element for &'static str {
         (layout_id, state)
     }
 
-    fn paint(self, bounds: Bounds<Pixels>, state: &mut TextState, cx: &mut WindowContext) {
+    fn paint(&mut self, bounds: Bounds<Pixels>, state: &mut TextState, cx: &mut WindowContext) {
         state.paint(bounds, self, cx)
     }
 }
@@ -52,7 +52,7 @@ impl Element for SharedString {
         (layout_id, state)
     }
 
-    fn paint(self, bounds: Bounds<Pixels>, state: &mut TextState, cx: &mut WindowContext) {
+    fn paint(&mut self, bounds: Bounds<Pixels>, state: &mut TextState, cx: &mut WindowContext) {
         let text_str: &str = self.as_ref();
         state.paint(bounds, text_str, cx)
     }
@@ -128,7 +128,7 @@ impl Element for StyledText {
         (layout_id, state)
     }
 
-    fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
+    fn paint(&mut self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
         state.paint(bounds, &self.text, cx)
     }
 }
@@ -356,8 +356,8 @@ impl Element for InteractiveText {
         }
     }
 
-    fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
-        if let Some(click_listener) = self.click_listener {
+    fn paint(&mut self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
+        if let Some(click_listener) = self.click_listener.take() {
             if let Some(ix) = state
                 .text_state
                 .index_for_position(bounds, cx.mouse_position())
@@ -374,13 +374,14 @@ impl Element for InteractiveText {
             let text_state = state.text_state.clone();
             let mouse_down = state.mouse_down_index.clone();
             if let Some(mouse_down_index) = mouse_down.get() {
+                let clickable_ranges = mem::take(&mut self.clickable_ranges);
                 cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| {
                     if phase == DispatchPhase::Bubble {
                         if let Some(mouse_up_index) =
                             text_state.index_for_position(bounds, event.position)
                         {
                             click_listener(
-                                &self.clickable_ranges,
+                                &clickable_ranges,
                                 InteractiveTextClickEvent {
                                     mouse_down_index,
                                     mouse_up_index,

crates/gpui2/src/elements/uniform_list.rs πŸ”—

@@ -10,6 +10,7 @@ use taffy::style::Overflow;
 /// uniform_list provides lazy rendering for a set of items that are of uniform height.
 /// When rendered into a container with overflow-y: hidden and a fixed (or max) height,
 /// uniform_list will only render the visible subset of items.
+#[track_caller]
 pub fn uniform_list<I, R, V>(
     view: View<V>,
     id: I,
@@ -42,6 +43,10 @@ where
         interactivity: Interactivity {
             element_id: Some(id.into()),
             base_style: Box::new(base_style),
+
+            #[cfg(debug_assertions)]
+            location: Some(*core::panic::Location::caller()),
+
             ..Default::default()
         },
         scroll_handle: None,
@@ -150,7 +155,7 @@ impl Element for UniformList {
     }
 
     fn paint(
-        self,
+        &mut self,
         bounds: Bounds<crate::Pixels>,
         element_state: &mut Self::State,
         cx: &mut WindowContext,
@@ -197,41 +202,41 @@ impl Element for UniformList {
                 );
 
                 cx.with_z_index(style.z_index.unwrap_or(0), |cx| {
-                    style.paint(bounds, cx);
-
-                    if self.item_count > 0 {
-                        if let Some(scroll_handle) = self.scroll_handle.clone() {
-                            scroll_handle.0.borrow_mut().replace(ScrollHandleState {
-                                item_height,
-                                list_height: padded_bounds.size.height,
-                                scroll_offset: shared_scroll_offset,
+                    style.paint(bounds, cx, |cx| {
+                        if self.item_count > 0 {
+                            if let Some(scroll_handle) = self.scroll_handle.clone() {
+                                scroll_handle.0.borrow_mut().replace(ScrollHandleState {
+                                    item_height,
+                                    list_height: padded_bounds.size.height,
+                                    scroll_offset: shared_scroll_offset,
+                                });
+                            }
+
+                            let first_visible_element_ix =
+                                (-scroll_offset.y / item_height).floor() as usize;
+                            let last_visible_element_ix =
+                                ((-scroll_offset.y + padded_bounds.size.height) / item_height)
+                                    .ceil() as usize;
+                            let visible_range = first_visible_element_ix
+                                ..cmp::min(last_visible_element_ix, self.item_count);
+
+                            let mut items = (self.render_items)(visible_range.clone(), cx);
+                            cx.with_z_index(1, |cx| {
+                                let content_mask = ContentMask { bounds };
+                                cx.with_content_mask(Some(content_mask), |cx| {
+                                    for (item, ix) in items.iter_mut().zip(visible_range) {
+                                        let item_origin = padded_bounds.origin
+                                            + point(px(0.), item_height * ix + scroll_offset.y);
+                                        let available_space = size(
+                                            AvailableSpace::Definite(padded_bounds.size.width),
+                                            AvailableSpace::Definite(item_height),
+                                        );
+                                        item.draw(item_origin, available_space, cx);
+                                    }
+                                });
                             });
                         }
-
-                        let first_visible_element_ix =
-                            (-scroll_offset.y / item_height).floor() as usize;
-                        let last_visible_element_ix =
-                            ((-scroll_offset.y + padded_bounds.size.height) / item_height).ceil()
-                                as usize;
-                        let visible_range = first_visible_element_ix
-                            ..cmp::min(last_visible_element_ix, self.item_count);
-
-                        let items = (self.render_items)(visible_range.clone(), cx);
-                        cx.with_z_index(1, |cx| {
-                            let content_mask = ContentMask { bounds };
-                            cx.with_content_mask(Some(content_mask), |cx| {
-                                for (item, ix) in items.into_iter().zip(visible_range) {
-                                    let item_origin = padded_bounds.origin
-                                        + point(px(0.), item_height * ix + scroll_offset.y);
-                                    let available_space = size(
-                                        AvailableSpace::Definite(padded_bounds.size.width),
-                                        AvailableSpace::Definite(item_height),
-                                    );
-                                    item.draw(item_origin, available_space, cx);
-                                }
-                            });
-                        });
-                    }
+                    });
                 })
             },
         );

crates/gpui2/src/geometry.rs πŸ”—

@@ -1592,6 +1592,17 @@ impl Edges<Pixels> {
     }
 }
 
+impl Into<Edges<Pixels>> for f32 {
+    fn into(self) -> Edges<Pixels> {
+        Edges {
+            top: self.into(),
+            right: self.into(),
+            bottom: self.into(),
+            left: self.into(),
+        }
+    }
+}
+
 /// Represents the corners of a box in a 2D space, such as border radius.
 ///
 /// Each field represents the size of the corner on one side of the box: `top_left`, `top_right`, `bottom_right`, and `bottom_left`.
@@ -1808,6 +1819,28 @@ where
 
 impl<T> Copy for Corners<T> where T: Copy + Clone + Default + Debug {}
 
+impl Into<Corners<Pixels>> for f32 {
+    fn into(self) -> Corners<Pixels> {
+        Corners {
+            top_left: self.into(),
+            top_right: self.into(),
+            bottom_right: self.into(),
+            bottom_left: self.into(),
+        }
+    }
+}
+
+impl Into<Corners<Pixels>> for Pixels {
+    fn into(self) -> Corners<Pixels> {
+        Corners {
+            top_left: self,
+            top_right: self,
+            bottom_right: self,
+            bottom_left: self,
+        }
+    }
+}
+
 /// Represents a length in pixels, the base unit of measurement in the UI framework.
 ///
 /// `Pixels` is a value type that represents an absolute length in pixels, which is used

crates/gpui2/src/platform.rs πŸ”—

@@ -147,6 +147,7 @@ pub trait PlatformWindow {
     fn appearance(&self) -> WindowAppearance;
     fn display(&self) -> Rc<dyn PlatformDisplay>;
     fn mouse_position(&self) -> Point<Pixels>;
+    fn modifiers(&self) -> Modifiers;
     fn as_any_mut(&mut self) -> &mut dyn Any;
     fn set_input_handler(&mut self, input_handler: Box<dyn PlatformInputHandler>);
     fn clear_input_handler(&mut self);

crates/gpui2/src/platform/mac/metal_renderer.rs πŸ”—

@@ -187,6 +187,8 @@ impl MetalRenderer {
     }
 
     pub fn draw(&mut self, scene: &Scene) {
+        let start = std::time::Instant::now();
+
         let layer = self.layer.clone();
         let viewport_size = layer.drawable_size();
         let viewport_size: Size<DevicePixels> = size(
@@ -303,6 +305,10 @@ impl MetalRenderer {
 
         command_buffer.commit();
         self.sprite_atlas.clear_textures(AtlasTextureKind::Path);
+
+        let duration_since_start = start.elapsed();
+        println!("renderer draw: {:?}", duration_since_start);
+
         command_buffer.wait_until_completed();
         drawable.present();
     }

crates/gpui2/src/platform/mac/window.rs πŸ”—

@@ -9,9 +9,10 @@ use crate::{
 use block::ConcreteBlock;
 use cocoa::{
     appkit::{
-        CGPoint, NSApplication, NSBackingStoreBuffered, NSFilenamesPboardType, NSPasteboard,
-        NSScreen, NSView, NSViewHeightSizable, NSViewWidthSizable, NSWindow, NSWindowButton,
-        NSWindowCollectionBehavior, NSWindowStyleMask, NSWindowTitleVisibility,
+        CGPoint, NSApplication, NSBackingStoreBuffered, NSEventModifierFlags,
+        NSFilenamesPboardType, NSPasteboard, NSScreen, NSView, NSViewHeightSizable,
+        NSViewWidthSizable, NSWindow, NSWindowButton, NSWindowCollectionBehavior,
+        NSWindowStyleMask, NSWindowTitleVisibility,
     },
     base::{id, nil},
     foundation::{
@@ -744,6 +745,26 @@ impl PlatformWindow for MacWindow {
         convert_mouse_position(position, self.content_size().height)
     }
 
+    fn modifiers(&self) -> Modifiers {
+        unsafe {
+            let modifiers: NSEventModifierFlags = msg_send![class!(NSEvent), modifierFlags];
+
+            let control = modifiers.contains(NSEventModifierFlags::NSControlKeyMask);
+            let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask);
+            let shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask);
+            let command = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask);
+            let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask);
+
+            Modifiers {
+                control,
+                alt,
+                shift,
+                command,
+                function,
+            }
+        }
+    }
+
     fn as_any_mut(&mut self) -> &mut dyn Any {
         self
     }

crates/gpui2/src/platform/test/window.rs πŸ”—

@@ -79,6 +79,10 @@ impl PlatformWindow for TestWindow {
         Point::default()
     }
 
+    fn modifiers(&self) -> crate::Modifiers {
+        crate::Modifiers::default()
+    }
+
     fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
         self
     }

crates/gpui2/src/style.rs πŸ”—

@@ -1,9 +1,9 @@
 use std::{iter, mem, ops::Range};
 
 use crate::{
-    black, phi, point, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds, ContentMask,
-    Corners, CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement, Font,
-    FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba,
+    black, phi, point, quad, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds,
+    ContentMask, Corners, CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement,
+    Font, FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba,
     SharedString, Size, SizeRefinement, Styled, TextRun, WindowContext,
 };
 use collections::HashSet;
@@ -14,6 +14,9 @@ pub use taffy::style::{
     Overflow, Position,
 };
 
+#[cfg(debug_assertions)]
+pub struct DebugBelow;
+
 pub type StyleCascade = Cascade<Style>;
 
 #[derive(Clone, Refineable, Debug)]
@@ -107,7 +110,12 @@ pub struct Style {
     /// The mouse cursor style shown when the mouse pointer is over an element.
     pub mouse_cursor: Option<CursorStyle>,
 
-    pub z_index: Option<u32>,
+    pub z_index: Option<u8>,
+
+    #[cfg(debug_assertions)]
+    pub debug: bool,
+    #[cfg(debug_assertions)]
+    pub debug_below: bool,
 }
 
 impl Styled for StyleRefinement {
@@ -334,7 +342,22 @@ impl Style {
     }
 
     /// Paints the background of an element styled with this style.
-    pub fn paint(&self, bounds: Bounds<Pixels>, cx: &mut WindowContext) {
+    pub fn paint(
+        &self,
+        bounds: Bounds<Pixels>,
+        cx: &mut WindowContext,
+        continuation: impl FnOnce(&mut WindowContext),
+    ) {
+        #[cfg(debug_assertions)]
+        if self.debug_below {
+            cx.set_global(DebugBelow)
+        }
+
+        #[cfg(debug_assertions)]
+        if self.debug || cx.has_global::<DebugBelow>() {
+            cx.paint_quad(crate::outline(bounds, crate::red()));
+        }
+
         let rem_size = cx.rem_size();
 
         cx.with_z_index(0, |cx| {
@@ -348,15 +371,24 @@ impl Style {
         let background_color = self.background.as_ref().and_then(Fill::color);
         if background_color.is_some() || self.is_border_visible() {
             cx.with_z_index(1, |cx| {
-                cx.paint_quad(
+                cx.paint_quad(quad(
                     bounds,
                     self.corner_radii.to_pixels(bounds.size, rem_size),
                     background_color.unwrap_or_default(),
                     self.border_widths.to_pixels(rem_size),
                     self.border_color.unwrap_or_default(),
-                );
+                ));
             });
         }
+
+        cx.with_z_index(2, |cx| {
+            continuation(cx);
+        });
+
+        #[cfg(debug_assertions)]
+        if self.debug_below {
+            cx.remove_global::<DebugBelow>();
+        }
     }
 
     fn is_border_visible(&self) -> bool {
@@ -404,6 +436,11 @@ impl Default for Style {
             text: TextStyleRefinement::default(),
             mouse_cursor: None,
             z_index: None,
+
+            #[cfg(debug_assertions)]
+            debug: false,
+            #[cfg(debug_assertions)]
+            debug_below: false,
         }
     }
 }

crates/gpui2/src/styled.rs πŸ”—

@@ -12,7 +12,7 @@ pub trait Styled: Sized {
 
     gpui2_macros::style_helpers!();
 
-    fn z_index(mut self, z_index: u32) -> Self {
+    fn z_index(mut self, z_index: u8) -> Self {
         self.style().z_index = Some(z_index);
         self
     }
@@ -633,4 +633,16 @@ pub trait Styled: Sized {
             .line_height = Some(line_height.into());
         self
     }
+
+    #[cfg(debug_assertions)]
+    fn debug(mut self) -> Self {
+        self.style().debug = Some(true);
+        self
+    }
+
+    #[cfg(debug_assertions)]
+    fn debug_below(mut self) -> Self {
+        self.style().debug_below = Some(true);
+        self
+    }
 }

crates/gpui2/src/text_system/line.rs πŸ”—

@@ -1,7 +1,6 @@
 use crate::{
-    black, point, px, size, transparent_black, BorrowWindow, Bounds, Corners, Edges, Hsla,
-    LineLayout, Pixels, Point, Result, SharedString, UnderlineStyle, WindowContext, WrapBoundary,
-    WrappedLineLayout,
+    black, fill, point, px, size, BorrowWindow, Bounds, Hsla, LineLayout, Pixels, Point, Result,
+    SharedString, UnderlineStyle, WindowContext, WrapBoundary, WrappedLineLayout,
 };
 use derive_more::{Deref, DerefMut};
 use smallvec::SmallVec;
@@ -109,16 +108,13 @@ fn paint_line(
             if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
                 wraps.next();
                 if let Some((background_origin, background_color)) = current_background.as_mut() {
-                    cx.paint_quad(
+                    cx.paint_quad(fill(
                         Bounds {
                             origin: *background_origin,
                             size: size(glyph_origin.x - background_origin.x, line_height),
                         },
-                        Corners::default(),
                         *background_color,
-                        Edges::default(),
-                        transparent_black(),
-                    );
+                    ));
                     background_origin.x = origin.x;
                     background_origin.y += line_height;
                 }
@@ -180,16 +176,13 @@ fn paint_line(
             }
 
             if let Some((background_origin, background_color)) = finished_background {
-                cx.paint_quad(
+                cx.paint_quad(fill(
                     Bounds {
                         origin: background_origin,
                         size: size(glyph_origin.x - background_origin.x, line_height),
                     },
-                    Corners::default(),
                     background_color,
-                    Edges::default(),
-                    transparent_black(),
-                );
+                ));
             }
 
             if let Some((underline_origin, underline_style)) = finished_underline {
@@ -235,16 +228,13 @@ fn paint_line(
     }
 
     if let Some((background_origin, background_color)) = current_background.take() {
-        cx.paint_quad(
+        cx.paint_quad(fill(
             Bounds {
                 origin: background_origin,
                 size: size(last_line_end_x - background_origin.x, line_height),
             },
-            Corners::default(),
             background_color,
-            Edges::default(),
-            transparent_black(),
-        );
+        ));
     }
 
     if let Some((underline_start, underline_style)) = current_underline.take() {

crates/gpui2/src/view.rs πŸ”—

@@ -7,6 +7,7 @@ use crate::{
 use anyhow::{Context, Result};
 use std::{
     any::TypeId,
+    fmt,
     hash::{Hash, Hasher},
 };
 
@@ -90,7 +91,7 @@ impl<V: Render> Element for View<V> {
         (layout_id, Some(element))
     }
 
-    fn paint(self, _: Bounds<Pixels>, element: &mut Self::State, cx: &mut WindowContext) {
+    fn paint(&mut self, _: Bounds<Pixels>, element: &mut Self::State, cx: &mut WindowContext) {
         element.take().unwrap().paint(cx);
     }
 }
@@ -170,7 +171,7 @@ impl<V> Eq for WeakView<V> {}
 pub struct AnyView {
     model: AnyModel,
     layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, AnyElement),
-    paint: fn(&AnyView, AnyElement, &mut WindowContext),
+    paint: fn(&AnyView, &mut AnyElement, &mut WindowContext),
 }
 
 impl AnyView {
@@ -208,9 +209,20 @@ impl AnyView {
         cx: &mut WindowContext,
     ) {
         cx.with_absolute_element_offset(origin, |cx| {
-            let (layout_id, rendered_element) = (self.layout)(self, cx);
+            let start_time = std::time::Instant::now();
+            let (layout_id, mut rendered_element) = (self.layout)(self, cx);
+            let duration = start_time.elapsed();
+            println!("request layout: {:?}", duration);
+
+            let start_time = std::time::Instant::now();
             cx.compute_layout(layout_id, available_space);
-            (self.paint)(self, rendered_element, cx);
+            let duration = start_time.elapsed();
+            println!("compute layout: {:?}", duration);
+
+            let start_time = std::time::Instant::now();
+            (self.paint)(self, &mut rendered_element, cx);
+            let duration = start_time.elapsed();
+            println!("paint: {:?}", duration);
         })
     }
 }
@@ -237,12 +249,12 @@ impl Element for AnyView {
         (layout_id, Some(state))
     }
 
-    fn paint(self, _: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
+    fn paint(&mut self, _: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
         debug_assert!(
             state.is_some(),
             "state is None. Did you include an AnyView twice in the tree?"
         );
-        (self.paint)(&self, state.take().unwrap(), cx)
+        (self.paint)(&self, state.as_mut().unwrap(), cx)
     }
 }
 
@@ -273,7 +285,7 @@ impl IntoElement for AnyView {
 pub struct AnyWeakView {
     model: AnyWeakModel,
     layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, AnyElement),
-    paint: fn(&AnyView, AnyElement, &mut WindowContext),
+    paint: fn(&AnyView, &mut AnyElement, &mut WindowContext),
 }
 
 impl AnyWeakView {
@@ -297,6 +309,20 @@ impl<V: 'static + Render> From<WeakView<V>> for AnyWeakView {
     }
 }
 
+impl PartialEq for AnyWeakView {
+    fn eq(&self, other: &Self) -> bool {
+        self.model == other.model
+    }
+}
+
+impl std::fmt::Debug for AnyWeakView {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.debug_struct("AnyWeakView")
+            .field("entity_id", &self.model.entity_id)
+            .finish_non_exhaustive()
+    }
+}
+
 impl<T, E> Render for T
 where
     T: 'static + FnMut(&mut WindowContext) -> E,
@@ -324,7 +350,7 @@ mod any_view {
 
     pub(crate) fn paint<V: 'static + Render>(
         _view: &AnyView,
-        element: AnyElement,
+        element: &mut AnyElement,
         cx: &mut WindowContext,
     ) {
         element.paint(cx);

crates/gpui2/src/window.rs πŸ”—

@@ -1,18 +1,18 @@
 use crate::{
-    key_dispatch::DispatchActionListener, px, size, Action, AnyDrag, AnyView, AppContext,
-    AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle,
-    DevicePixels, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId,
-    EventEmitter, FileDropEvent, Flatten, FocusEvent, FontId, GlobalElementId, GlyphId, Hsla,
-    ImageData, InputEvent, IsZero, KeyBinding, KeyContext, KeyDownEvent, KeystrokeEvent, LayoutId,
-    Model, ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseMoveEvent, MouseUpEvent,
-    Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point,
-    PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams,
-    RenderSvgParams, ScaledPixels, Scene, SceneBuilder, Shadow, SharedString, Size, Style,
-    SubscriberSet, Subscription, Surface, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View,
-    VisualContext, WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS,
+    key_dispatch::DispatchActionListener, px, size, transparent_black, Action, AnyDrag, AnyView,
+    AppContext, AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners,
+    CursorStyle, DevicePixels, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity,
+    EntityId, EventEmitter, FileDropEvent, Flatten, FocusEvent, FontId, GlobalElementId, GlyphId,
+    Hsla, ImageData, InputEvent, IsZero, KeyBinding, KeyContext, KeyDownEvent, KeystrokeEvent,
+    LayoutId, Model, ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseMoveEvent,
+    MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler,
+    PlatformWindow, Point, PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams,
+    RenderImageParams, RenderSvgParams, ScaledPixels, Scene, SceneBuilder, Shadow, SharedString,
+    Size, Style, SubscriberSet, Subscription, Surface, TaffyLayoutEngine, Task, Underline,
+    UnderlineStyle, View, VisualContext, WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS,
 };
 use anyhow::{anyhow, Context as _, Result};
-use collections::HashMap;
+use collections::FxHashMap;
 use derive_more::{Deref, DerefMut};
 use futures::{
     channel::{mpsc, oneshot},
@@ -38,12 +38,24 @@ use std::{
 };
 use util::ResultExt;
 
-const ACTIVE_DRAG_Z_INDEX: u32 = 1;
+const ACTIVE_DRAG_Z_INDEX: u8 = 1;
 
 /// A global stacking order, which is created by stacking successive z-index values.
 /// Each z-index will always be interpreted in the context of its parent z-index.
-#[derive(Deref, DerefMut, Ord, PartialOrd, Eq, PartialEq, Clone, Default, Debug)]
-pub struct StackingOrder(pub(crate) SmallVec<[u32; 16]>);
+#[derive(Deref, DerefMut, Clone, Debug, Ord, PartialOrd, PartialEq, Eq)]
+pub struct StackingOrder {
+    #[deref]
+    #[deref_mut]
+    z_indices: SmallVec<[u8; 64]>,
+}
+
+impl Default for StackingOrder {
+    fn default() -> Self {
+        StackingOrder {
+            z_indices: SmallVec::new(),
+        }
+    }
+}
 
 /// Represents the two different phases when dispatching events.
 #[derive(Default, Copy, Clone, Debug, Eq, PartialEq)]
@@ -231,12 +243,14 @@ pub struct Window {
     pub(crate) blur_listeners: SubscriberSet<(), AnyObserver>,
     default_prevented: bool,
     mouse_position: Point<Pixels>,
+    modifiers: Modifiers,
     requested_cursor_style: Option<CursorStyle>,
     scale_factor: f32,
     bounds: WindowBounds,
     bounds_observers: SubscriberSet<(), AnyObserver>,
     active: bool,
     pub(crate) dirty: bool,
+    pub(crate) drawing: bool,
     activation_observers: SubscriberSet<(), AnyObserver>,
     pub(crate) last_blur: Option<Option<FocusId>>,
     pub(crate) focus: Option<FocusId>,
@@ -250,8 +264,8 @@ pub(crate) struct ElementStateBox {
 
 // #[derive(Default)]
 pub(crate) struct Frame {
-    pub(crate) element_states: HashMap<GlobalElementId, ElementStateBox>,
-    mouse_listeners: HashMap<TypeId, Vec<(StackingOrder, AnyMouseListener)>>,
+    pub(crate) element_states: FxHashMap<GlobalElementId, ElementStateBox>,
+    mouse_listeners: FxHashMap<TypeId, Vec<(StackingOrder, AnyMouseListener)>>,
     pub(crate) dispatch_tree: DispatchTree,
     pub(crate) focus_listeners: Vec<AnyFocusListener>,
     pub(crate) scene_builder: SceneBuilder,
@@ -264,8 +278,8 @@ pub(crate) struct Frame {
 impl Frame {
     fn new(dispatch_tree: DispatchTree) -> Self {
         Frame {
-            element_states: HashMap::default(),
-            mouse_listeners: HashMap::default(),
+            element_states: FxHashMap::default(),
+            mouse_listeners: FxHashMap::default(),
             dispatch_tree,
             focus_listeners: Vec::new(),
             scene_builder: SceneBuilder::default(),
@@ -302,6 +316,7 @@ impl Window {
         let display_id = platform_window.display().id();
         let sprite_atlas = platform_window.sprite_atlas();
         let mouse_position = platform_window.mouse_position();
+        let modifiers = platform_window.modifiers();
         let content_size = platform_window.content_size();
         let scale_factor = platform_window.scale_factor();
         let bounds = platform_window.bounds();
@@ -365,12 +380,14 @@ impl Window {
             blur_listeners: SubscriberSet::new(),
             default_prevented: true,
             mouse_position,
+            modifiers,
             requested_cursor_style: None,
             scale_factor,
             bounds,
             bounds_observers: SubscriberSet::new(),
             active: false,
             dirty: false,
+            drawing: false,
             activation_observers: SubscriberSet::new(),
             last_blur: None,
             focus: None,
@@ -422,7 +439,9 @@ impl<'a> WindowContext<'a> {
 
     /// Mark the window as dirty, scheduling it to be redrawn on the next frame.
     pub fn notify(&mut self) {
-        self.window.dirty = true;
+        if !self.window.drawing {
+            self.window.dirty = true;
+        }
     }
 
     /// Close this window.
@@ -803,7 +822,7 @@ impl<'a> WindowContext<'a> {
     /// a specific need to register a global listener.
     pub fn on_mouse_event<Event: 'static>(
         &mut self,
-        handler: impl Fn(&Event, DispatchPhase, &mut WindowContext) + 'static,
+        mut handler: impl FnMut(&Event, DispatchPhase, &mut WindowContext) + 'static,
     ) {
         let order = self.window.next_frame.z_index_stack.clone();
         self.window
@@ -877,13 +896,18 @@ impl<'a> WindowContext<'a> {
         self.window.mouse_position
     }
 
+    /// The current state of the keyboard's modifiers
+    pub fn modifiers(&self) -> Modifiers {
+        self.window.modifiers
+    }
+
     pub fn set_cursor_style(&mut self, style: CursorStyle) {
         self.window.requested_cursor_style = Some(style)
     }
 
     /// Called during painting to invoke the given closure in a new stacking context. The given
     /// z-index is interpreted relative to the previous call to `stack`.
-    pub fn with_z_index<R>(&mut self, z_index: u32, f: impl FnOnce(&mut Self) -> R) -> R {
+    pub fn with_z_index<R>(&mut self, z_index: u8, f: impl FnOnce(&mut Self) -> R) -> R {
         self.window.next_frame.z_index_stack.push(z_index);
         let result = f(self);
         self.window.next_frame.z_index_stack.pop();
@@ -963,14 +987,8 @@ impl<'a> WindowContext<'a> {
 
     /// Paint one or more quads into the scene for the next frame at the current stacking context.
     /// Quads are colored rectangular regions with an optional background, border, and corner radius.
-    pub fn paint_quad(
-        &mut self,
-        bounds: Bounds<Pixels>,
-        corner_radii: Corners<Pixels>,
-        background: impl Into<Hsla>,
-        border_widths: Edges<Pixels>,
-        border_color: impl Into<Hsla>,
-    ) {
+    /// see [`fill`], [`outline`], and [`quad`] to construct this type.
+    pub fn paint_quad(&mut self, quad: PaintQuad) {
         let scale_factor = self.scale_factor();
         let content_mask = self.content_mask();
 
@@ -979,12 +997,12 @@ impl<'a> WindowContext<'a> {
             &window.next_frame.z_index_stack,
             Quad {
                 order: 0,
-                bounds: bounds.scale(scale_factor),
+                bounds: quad.bounds.scale(scale_factor),
                 content_mask: content_mask.scale(scale_factor),
-                background: background.into(),
-                border_color: border_color.into(),
-                corner_radii: corner_radii.scale(scale_factor),
-                border_widths: border_widths.scale(scale_factor),
+                background: quad.background,
+                border_color: quad.border_color,
+                corner_radii: quad.corner_radii.scale(scale_factor),
+                border_widths: quad.border_widths.scale(scale_factor),
             },
         );
     }
@@ -1236,6 +1254,10 @@ impl<'a> WindowContext<'a> {
 
     /// Draw pixels to the display for this window based on the contents of its scene.
     pub(crate) fn draw(&mut self) -> Scene {
+        let t0 = std::time::Instant::now();
+        self.window.dirty = false;
+        self.window.drawing = true;
+
         let window_was_focused = self
             .window
             .focus
@@ -1325,7 +1347,8 @@ impl<'a> WindowContext<'a> {
             self.platform.set_cursor_style(cursor_style);
         }
 
-        self.window.dirty = false;
+        self.window.drawing = false;
+        eprintln!("window draw: {:?}", t0.elapsed());
 
         scene
     }
@@ -1342,16 +1365,34 @@ impl<'a> WindowContext<'a> {
             // API for the mouse position can only occur on the main thread.
             InputEvent::MouseMove(mouse_move) => {
                 self.window.mouse_position = mouse_move.position;
+                self.window.modifiers = mouse_move.modifiers;
                 InputEvent::MouseMove(mouse_move)
             }
             InputEvent::MouseDown(mouse_down) => {
                 self.window.mouse_position = mouse_down.position;
+                self.window.modifiers = mouse_down.modifiers;
                 InputEvent::MouseDown(mouse_down)
             }
             InputEvent::MouseUp(mouse_up) => {
                 self.window.mouse_position = mouse_up.position;
+                self.window.modifiers = mouse_up.modifiers;
                 InputEvent::MouseUp(mouse_up)
             }
+            InputEvent::MouseExited(mouse_exited) => {
+                // todo!("Should we record that the mouse is outside of the window somehow? Or are these global pixels?")
+                self.window.modifiers = mouse_exited.modifiers;
+
+                InputEvent::MouseExited(mouse_exited)
+            }
+            InputEvent::ModifiersChanged(modifiers_changed) => {
+                self.window.modifiers = modifiers_changed.modifiers;
+                InputEvent::ModifiersChanged(modifiers_changed)
+            }
+            InputEvent::ScrollWheel(scroll_wheel) => {
+                self.window.mouse_position = scroll_wheel.position;
+                self.window.modifiers = scroll_wheel.modifiers;
+                InputEvent::ScrollWheel(scroll_wheel)
+            }
             // Translate dragging and dropping of external files from the operating system
             // to internal drag and drop events.
             InputEvent::FileDrop(file_drop) => match file_drop {
@@ -1359,6 +1400,7 @@ impl<'a> WindowContext<'a> {
                     self.window.mouse_position = position;
                     if self.active_drag.is_none() {
                         self.active_drag = Some(AnyDrag {
+                            value: Box::new(files.clone()),
                             view: self.build_view(|_| files).into(),
                             cursor_offset: position,
                         });
@@ -1394,7 +1436,7 @@ impl<'a> WindowContext<'a> {
                     click_count: 1,
                 }),
             },
-            _ => event,
+            InputEvent::KeyDown(_) | InputEvent::KeyUp(_) => event,
         };
 
         if let Some(any_mouse_event) = event.mouse_event() {
@@ -2206,7 +2248,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
         &mut self.window_cx
     }
 
-    pub fn with_z_index<R>(&mut self, z_index: u32, f: impl FnOnce(&mut Self) -> R) -> R {
+    pub fn with_z_index<R>(&mut self, z_index: u8, f: impl FnOnce(&mut Self) -> R) -> R {
         self.window.next_frame.z_index_stack.push(z_index);
         let result = f(self);
         self.window.next_frame.z_index_stack.pop();
@@ -2343,10 +2385,12 @@ impl<'a, V: 'static> ViewContext<'a, V> {
     }
 
     pub fn notify(&mut self) {
-        self.window_cx.notify();
-        self.window_cx.app.push_effect(Effect::Notify {
-            emitter: self.view.model.entity_id,
-        });
+        if !self.window.drawing {
+            self.window_cx.notify();
+            self.window_cx.app.push_effect(Effect::Notify {
+                emitter: self.view.model.entity_id,
+            });
+        }
     }
 
     pub fn observe_window_bounds(
@@ -2881,12 +2925,12 @@ impl AnyWindowHandle {
     }
 }
 
-#[cfg(any(test, feature = "test-support"))]
-impl From<SmallVec<[u32; 16]>> for StackingOrder {
-    fn from(small_vec: SmallVec<[u32; 16]>) -> Self {
-        StackingOrder(small_vec)
-    }
-}
+// #[cfg(any(test, feature = "test-support"))]
+// impl From<SmallVec<[u32; 16]>> for StackingOrder {
+//     fn from(small_vec: SmallVec<[u32; 16]>) -> Self {
+//         StackingOrder(small_vec)
+//     }
+// }
 
 #[derive(Clone, Debug, Eq, PartialEq, Hash)]
 pub enum ElementId {
@@ -2962,3 +3006,85 @@ impl From<(&'static str, u64)> for ElementId {
         ElementId::NamedInteger(name.into(), id as usize)
     }
 }
+
+/// A rectangle, to be rendered on the screen by GPUI at the given position and size.
+pub struct PaintQuad {
+    bounds: Bounds<Pixels>,
+    corner_radii: Corners<Pixels>,
+    background: Hsla,
+    border_widths: Edges<Pixels>,
+    border_color: Hsla,
+}
+
+impl PaintQuad {
+    /// Set the corner radii of the quad.
+    pub fn corner_radii(self, corner_radii: impl Into<Corners<Pixels>>) -> Self {
+        PaintQuad {
+            corner_radii: corner_radii.into(),
+            ..self
+        }
+    }
+
+    /// Set the border widths of the quad.
+    pub fn border_widths(self, border_widths: impl Into<Edges<Pixels>>) -> Self {
+        PaintQuad {
+            border_widths: border_widths.into(),
+            ..self
+        }
+    }
+
+    /// Set the border color of the quad.
+    pub fn border_color(self, border_color: impl Into<Hsla>) -> Self {
+        PaintQuad {
+            border_color: border_color.into(),
+            ..self
+        }
+    }
+
+    /// Set the background color of the quad.
+    pub fn background(self, background: impl Into<Hsla>) -> Self {
+        PaintQuad {
+            background: background.into(),
+            ..self
+        }
+    }
+}
+
+/// Create a quad with the given parameters.
+pub fn quad(
+    bounds: Bounds<Pixels>,
+    corner_radii: impl Into<Corners<Pixels>>,
+    background: impl Into<Hsla>,
+    border_widths: impl Into<Edges<Pixels>>,
+    border_color: impl Into<Hsla>,
+) -> PaintQuad {
+    PaintQuad {
+        bounds,
+        corner_radii: corner_radii.into(),
+        background: background.into(),
+        border_widths: border_widths.into(),
+        border_color: border_color.into(),
+    }
+}
+
+/// Create a filled quad with the given bounds and background color.
+pub fn fill(bounds: impl Into<Bounds<Pixels>>, background: impl Into<Hsla>) -> PaintQuad {
+    PaintQuad {
+        bounds: bounds.into(),
+        corner_radii: (0.).into(),
+        background: background.into(),
+        border_widths: (0.).into(),
+        border_color: transparent_black(),
+    }
+}
+
+/// Create a rectangle outline with the given bounds, border color, and a 1px border width
+pub fn outline(bounds: impl Into<Bounds<Pixels>>, border_color: impl Into<Hsla>) -> PaintQuad {
+    PaintQuad {
+        bounds: bounds.into(),
+        corner_radii: (0.).into(),
+        background: transparent_black(),
+        border_widths: (1.).into(),
+        border_color: border_color.into(),
+    }
+}

crates/gpui2_macros/src/derive_render.rs πŸ”—

@@ -0,0 +1,23 @@
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::{parse_macro_input, DeriveInput};
+
+pub fn derive_render(input: TokenStream) -> TokenStream {
+    let ast = parse_macro_input!(input as DeriveInput);
+    let type_name = &ast.ident;
+    let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl();
+
+    let gen = quote! {
+        impl #impl_generics gpui::Render for #type_name #type_generics
+        #where_clause
+        {
+            type Element = ();
+
+            fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
+                ()
+            }
+        }
+    };
+
+    gen.into()
+}

crates/gpui2_macros/src/gpui2_macros.rs πŸ”—

@@ -1,4 +1,5 @@
 mod derive_into_element;
+mod derive_render;
 mod register_action;
 mod style_helpers;
 mod test;
@@ -15,6 +16,11 @@ pub fn derive_into_element(input: TokenStream) -> TokenStream {
     derive_into_element::derive_into_element(input)
 }
 
+#[proc_macro_derive(Render)]
+pub fn derive_render(input: TokenStream) -> TokenStream {
+    derive_render::derive_render(input)
+}
+
 #[proc_macro]
 pub fn style_helpers(input: TokenStream) -> TokenStream {
     style_helpers::style_helpers(input)

crates/project_panel2/src/project_panel.rs πŸ”—

@@ -1377,33 +1377,28 @@ impl ProjectPanel {
             })
             .unwrap_or(theme.status().info);
 
+        let file_name = details.filename.clone();
+        let icon = details.icon.clone();
+        let depth = details.depth;
         div()
             .id(entry_id.to_proto() as usize)
-            .on_drag({
-                let details = details.clone();
-                move |cx| {
-                    let details = details.clone();
-                    cx.build_view(|_| DraggedProjectEntryView {
-                        details,
-                        width,
-                        entry_id,
-                    })
-                }
-            })
-            .drag_over::<DraggedProjectEntryView>(|style| {
-                style.bg(cx.theme().colors().ghost_element_hover)
+            .on_drag(entry_id, move |entry_id, cx| {
+                cx.build_view(|_| DraggedProjectEntryView {
+                    details: details.clone(),
+                    width,
+                    entry_id: *entry_id,
+                })
             })
-            .on_drop(cx.listener(
-                move |this, dragged_view: &View<DraggedProjectEntryView>, cx| {
-                    this.move_entry(dragged_view.read(cx).entry_id, entry_id, kind.is_file(), cx);
-                },
-            ))
+            .drag_over::<ProjectEntryId>(|style| style.bg(cx.theme().colors().ghost_element_hover))
+            .on_drop(cx.listener(move |this, dragged_id: &ProjectEntryId, cx| {
+                this.move_entry(*dragged_id, entry_id, kind.is_file(), cx);
+            }))
             .child(
                 ListItem::new(entry_id.to_proto() as usize)
-                    .indent_level(details.depth)
+                    .indent_level(depth)
                     .indent_step_size(px(settings.indent_size))
                     .selected(is_selected)
-                    .child(if let Some(icon) = &details.icon {
+                    .child(if let Some(icon) = &icon {
                         div().child(IconElement::from_path(icon.to_string()))
                     } else {
                         div()
@@ -1414,7 +1409,7 @@ impl ProjectPanel {
                         } else {
                             div()
                                 .text_color(filename_text_color)
-                                .child(Label::new(details.filename.clone()))
+                                .child(Label::new(file_name))
                         }
                         .ml_1(),
                     )

crates/search2/src/project_search.rs πŸ”—

@@ -10,11 +10,13 @@ use editor::{
     items::active_match_index, scroll::autoscroll::Autoscroll, Anchor, Editor, EditorEvent,
     MultiBuffer, SelectAll, MAX_TAB_TITLE_LEN,
 };
+use editor::{EditorElement, EditorStyle};
 use gpui::{
-    actions, div, white, AnyElement, AnyView, AppContext, Context as _, Div, Element, EntityId,
-    EventEmitter, FocusableView, InteractiveElement, IntoElement, KeyContext, Model, ModelContext,
-    ParentElement, PromptLevel, Render, SharedString, Styled, Subscription, Task, View,
-    ViewContext, VisualContext, WeakModel, WeakView, WindowContext,
+    actions, div, AnyElement, AnyView, AppContext, Context as _, Div, Element, EntityId,
+    EventEmitter, FocusableView, FontStyle, FontWeight, InteractiveElement, IntoElement,
+    KeyContext, Model, ModelContext, ParentElement, PromptLevel, Render, SharedString, Styled,
+    Subscription, Task, TextStyle, View, ViewContext, VisualContext, WeakModel, WeakView,
+    WhiteSpace, WindowContext,
 };
 use menu::Confirm;
 use project::{
@@ -23,6 +25,7 @@ use project::{
 };
 use semantic_index::{SemanticIndex, SemanticIndexStatus};
 
+use settings::Settings;
 use smol::stream::StreamExt;
 use std::{
     any::{Any, TypeId},
@@ -32,6 +35,7 @@ use std::{
     path::PathBuf,
     time::{Duration, Instant},
 };
+use theme::ThemeSettings;
 
 use ui::{
     h_stack, prelude::*, v_stack, Button, Icon, IconButton, IconElement, Label, LabelCommon,
@@ -1425,6 +1429,36 @@ impl ProjectSearchBar {
         };
         new_placeholder_text
     }
+
+    fn render_text_input(&self, editor: &View<Editor>, cx: &ViewContext<Self>) -> impl IntoElement {
+        let settings = ThemeSettings::get_global(cx);
+        let text_style = TextStyle {
+            color: if editor.read(cx).read_only() {
+                cx.theme().colors().text_disabled
+            } else {
+                cx.theme().colors().text
+            },
+            font_family: settings.ui_font.family.clone(),
+            font_features: settings.ui_font.features,
+            font_size: rems(0.875).into(),
+            font_weight: FontWeight::NORMAL,
+            font_style: FontStyle::Normal,
+            line_height: relative(1.3).into(),
+            background_color: None,
+            underline: None,
+            white_space: WhiteSpace::Normal,
+        };
+
+        EditorElement::new(
+            &editor,
+            EditorStyle {
+                background: cx.theme().colors().editor_background,
+                local_player: cx.theme().players().local(),
+                text: text_style,
+                ..Default::default()
+            },
+        )
+    }
 }
 
 impl Render for ProjectSearchBar {
@@ -1445,122 +1479,62 @@ impl Render for ProjectSearchBar {
         }
         let search = search.read(cx);
         let semantic_is_available = SemanticIndex::enabled(cx);
-        let query_column = v_stack()
-            //.flex_1()
-            .child(
-                h_stack()
-                    .min_w_80()
-                    .on_action(cx.listener(|this, action, cx| this.confirm(action, cx)))
-                    .on_action(
-                        cx.listener(|this, action, cx| this.previous_history_query(action, cx)),
-                    )
-                    .on_action(cx.listener(|this, action, cx| this.next_history_query(action, cx)))
-                    .child(IconElement::new(Icon::MagnifyingGlass))
-                    .child(search.query_editor.clone())
-                    .child(
-                        h_stack()
-                            .child(
-                                IconButton::new("project-search-filter-button", Icon::Filter)
-                                    .tooltip(|cx| {
-                                        Tooltip::for_action("Toggle filters", &ToggleFilters, cx)
-                                    })
-                                    .on_click(cx.listener(|this, _, cx| {
-                                        this.toggle_filters(cx);
-                                    }))
-                                    .selected(
-                                        self.active_project_search
-                                            .as_ref()
-                                            .map(|search| search.read(cx).filters_enabled)
-                                            .unwrap_or_default(),
-                                    ),
-                            )
-                            .when(search.current_mode != SearchMode::Semantic, |this| {
-                                this.child(
-                                    IconButton::new(
-                                        "project-search-case-sensitive",
-                                        Icon::CaseSensitive,
-                                    )
-                                    .tooltip(|cx| {
-                                        Tooltip::for_action(
-                                            "Toggle case sensitive",
-                                            &ToggleCaseSensitive,
-                                            cx,
-                                        )
-                                    })
-                                    .selected(
-                                        self.is_option_enabled(SearchOptions::CASE_SENSITIVE, cx),
-                                    )
-                                    .on_click(cx.listener(
-                                        |this, _, cx| {
-                                            this.toggle_search_option(
-                                                SearchOptions::CASE_SENSITIVE,
-                                                cx,
-                                            );
-                                        },
-                                    )),
-                                )
-                                .child(
-                                    IconButton::new("project-search-whole-word", Icon::WholeWord)
-                                        .tooltip(|cx| {
-                                            Tooltip::for_action(
-                                                "Toggle whole word",
-                                                &ToggleWholeWord,
-                                                cx,
-                                            )
-                                        })
-                                        .selected(
-                                            self.is_option_enabled(SearchOptions::WHOLE_WORD, cx),
-                                        )
-                                        .on_click(cx.listener(|this, _, cx| {
-                                            this.toggle_search_option(
-                                                SearchOptions::WHOLE_WORD,
-                                                cx,
-                                            );
-                                        })),
-                                )
-                            }),
-                    )
-                    .border_2()
-                    .bg(white())
-                    .rounded_lg(),
-            )
-            .when(search.filters_enabled, |this| {
-                this.child(
+        let query_column = v_stack().child(
+            h_stack()
+                .min_w(rems(512. / 16.))
+                .px_2()
+                .py_1()
+                .gap_2()
+                .bg(cx.theme().colors().editor_background)
+                .border_1()
+                .border_color(cx.theme().colors().border)
+                .rounded_lg()
+                .on_action(cx.listener(|this, action, cx| this.confirm(action, cx)))
+                .on_action(cx.listener(|this, action, cx| this.previous_history_query(action, cx)))
+                .on_action(cx.listener(|this, action, cx| this.next_history_query(action, cx)))
+                .child(IconElement::new(Icon::MagnifyingGlass))
+                .child(self.render_text_input(&search.query_editor, cx))
+                .child(
                     h_stack()
-                        .mt_2()
-                        .flex_1()
-                        .justify_between()
                         .child(
-                            h_stack()
-                                .flex_1()
-                                .border_1()
-                                .mr_2()
-                                .child(search.included_files_editor.clone())
-                                .when(search.current_mode != SearchMode::Semantic, |this| {
-                                    this.child(
-                                        SearchOptions::INCLUDE_IGNORED.as_button(
-                                            search
-                                                .search_options
-                                                .contains(SearchOptions::INCLUDE_IGNORED),
-                                            cx.listener(|this, _, cx| {
-                                                this.toggle_search_option(
-                                                    SearchOptions::INCLUDE_IGNORED,
-                                                    cx,
-                                                );
-                                            }),
-                                        ),
-                                    )
-                                }),
+                            IconButton::new("project-search-filter-button", Icon::Filter)
+                                .tooltip(|cx| {
+                                    Tooltip::for_action("Toggle filters", &ToggleFilters, cx)
+                                })
+                                .on_click(cx.listener(|this, _, cx| {
+                                    this.toggle_filters(cx);
+                                }))
+                                .selected(
+                                    self.active_project_search
+                                        .as_ref()
+                                        .map(|search| search.read(cx).filters_enabled)
+                                        .unwrap_or_default(),
+                                ),
                         )
-                        .child(
-                            h_stack()
-                                .flex_1()
-                                .border_1()
-                                .ml_2()
-                                .child(search.excluded_files_editor.clone()),
-                        ),
-                )
-            });
+                        .when(search.current_mode != SearchMode::Semantic, |this| {
+                            this.child(
+                                IconButton::new(
+                                    "project-search-case-sensitive",
+                                    Icon::CaseSensitive,
+                                )
+                                .tooltip(|cx| {
+                                    Tooltip::for_action(
+                                        "Toggle case sensitive",
+                                        &ToggleCaseSensitive,
+                                        cx,
+                                    )
+                                })
+                                .selected(self.is_option_enabled(SearchOptions::WHOLE_WORD, cx))
+                                .on_click(cx.listener(
+                                    |this, _, cx| {
+                                        this.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
+                                    },
+                                )),
+                            )
+                        }),
+                ),
+        );
+
         let mode_column = v_stack().items_start().justify_start().child(
             h_stack()
                 .child(
@@ -1616,12 +1590,16 @@ impl Render for ProjectSearchBar {
         );
         let replace_column = if search.replace_enabled {
             h_stack()
-                .p_1()
                 .flex_1()
-                .border_2()
+                .h_full()
+                .gap_2()
+                .px_2()
+                .py_1()
+                .border_1()
+                .border_color(cx.theme().colors().border)
                 .rounded_lg()
                 .child(IconElement::new(Icon::Replace).size(ui::IconSize::Small))
-                .child(search.replacement_editor.clone())
+                .child(self.render_text_input(&search.replacement_editor, cx))
         } else {
             // Fill out the space if we don't have a replacement editor.
             h_stack().flex_1()
@@ -1682,12 +1660,12 @@ impl Render for ProjectSearchBar {
                     }))
                     .tooltip(|cx| Tooltip::for_action("Go to next match", &SelectNextMatch, cx)),
             ]);
-        h_stack()
+        v_stack()
             .key_context(key_context)
-            .size_full()
+            .flex_grow()
             .p_1()
             .m_2()
-            .justify_between()
+            .gap_2()
             .on_action(cx.listener(|this, _: &ToggleFilters, cx| {
                 this.toggle_filters(cx);
             }))
@@ -1739,10 +1717,59 @@ impl Render for ProjectSearchBar {
                     }))
                 })
             })
-            .child(query_column)
-            .child(mode_column)
-            .child(replace_column)
-            .child(actions_column)
+            .child(
+                h_stack()
+                    .justify_between()
+                    .child(query_column)
+                    .child(mode_column)
+                    .child(replace_column)
+                    .child(actions_column),
+            )
+            .when(search.filters_enabled, |this| {
+                this.child(
+                    h_stack()
+                        .flex_1()
+                        .gap_2()
+                        .justify_between()
+                        .child(
+                            h_stack()
+                                .flex_1()
+                                .h_full()
+                                .px_2()
+                                .py_1()
+                                .border_1()
+                                .border_color(cx.theme().colors().border)
+                                .rounded_lg()
+                                .child(self.render_text_input(&search.included_files_editor, cx))
+                                .when(search.current_mode != SearchMode::Semantic, |this| {
+                                    this.child(
+                                        SearchOptions::INCLUDE_IGNORED.as_button(
+                                            search
+                                                .search_options
+                                                .contains(SearchOptions::INCLUDE_IGNORED),
+                                            cx.listener(|this, _, cx| {
+                                                this.toggle_search_option(
+                                                    SearchOptions::INCLUDE_IGNORED,
+                                                    cx,
+                                                );
+                                            }),
+                                        ),
+                                    )
+                                }),
+                        )
+                        .child(
+                            h_stack()
+                                .flex_1()
+                                .h_full()
+                                .px_2()
+                                .py_1()
+                                .border_1()
+                                .border_color(cx.theme().colors().border)
+                                .rounded_lg()
+                                .child(self.render_text_input(&search.excluded_files_editor, cx)),
+                        ),
+                )
+            })
     }
 }
 // impl Entity for ProjectSearchBar {

crates/storybook2/Cargo.toml πŸ”—

@@ -12,9 +12,10 @@ path = "src/storybook2.rs"
 anyhow.workspace = true
 # TODO: Remove after diagnosing stack overflow.
 backtrace-on-stack-overflow = "0.3.0"
+chrono = "0.4"
 clap = { version = "4.4", features = ["derive", "string"] }
+dialoguer = { version = "0.11.0", features = ["fuzzy-select"] }
 editor = { package = "editor2", path = "../editor2" }
-chrono = "0.4"
 fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
 gpui = { package = "gpui2", path = "../gpui2" }
 itertools = "0.11.0"

crates/storybook2/src/stories/z_index.rs πŸ”—

@@ -78,7 +78,7 @@ impl Styles for Div {}
 
 #[derive(IntoElement)]
 struct ZIndexExample {
-    z_index: u32,
+    z_index: u8,
 }
 
 impl RenderOnce for ZIndexExample {
@@ -170,7 +170,7 @@ impl RenderOnce for ZIndexExample {
 }
 
 impl ZIndexExample {
-    pub fn new(z_index: u32) -> Self {
+    pub fn new(z_index: u8) -> Self {
         Self { z_index }
     }
 }

crates/storybook2/src/storybook2.rs πŸ”—

@@ -5,6 +5,7 @@ mod story_selector;
 use std::sync::Arc;
 
 use clap::Parser;
+use dialoguer::FuzzySelect;
 use gpui::{
     div, px, size, AnyView, AppContext, Bounds, Div, Render, ViewContext, VisualContext,
     WindowBounds, WindowOptions,
@@ -12,11 +13,12 @@ use gpui::{
 use log::LevelFilter;
 use settings2::{default_settings, Settings, SettingsStore};
 use simplelog::SimpleLogger;
+use strum::IntoEnumIterator;
 use theme2::{ThemeRegistry, ThemeSettings};
 use ui::prelude::*;
 
 use crate::assets::Assets;
-use crate::story_selector::StorySelector;
+use crate::story_selector::{ComponentStory, StorySelector};
 
 // gpui::actions! {
 //     storybook,
@@ -43,7 +45,17 @@ fn main() {
 
     let args = Args::parse();
 
-    let story_selector = args.story.clone();
+    let story_selector = args.story.clone().unwrap_or_else(|| {
+        let stories = ComponentStory::iter().collect::<Vec<_>>();
+
+        let selection = FuzzySelect::new()
+            .with_prompt("Choose a story to run:")
+            .items(&stories)
+            .interact()
+            .unwrap();
+
+        StorySelector::Component(stories[selection])
+    });
     let theme_name = args.theme.unwrap_or("One Dark".to_string());
 
     let asset_source = Arc::new(Assets);
@@ -58,7 +70,7 @@ fn main() {
 
         theme2::init(theme2::LoadThemes::All, cx);
 
-        let selector = story_selector.unwrap_or(StorySelector::KitchenSink);
+        let selector = story_selector;
 
         let theme_registry = cx.global::<ThemeRegistry>();
         let mut theme_settings = ThemeSettings::get_global(cx).clone();

crates/terminal_view2/src/terminal_element.rs πŸ”—

@@ -1,8 +1,8 @@
 use editor::{Cursor, HighlightedRange, HighlightedRangeLine};
 use gpui::{
-    black, div, point, px, red, relative, transparent_black, AnyElement, AsyncWindowContext,
-    AvailableSpace, Bounds, DispatchPhase, Element, ElementId, ExternalPaths, FocusHandle, Font,
-    FontStyle, FontWeight, HighlightStyle, Hsla, InteractiveElement, InteractiveElementState,
+    black, div, fill, point, px, red, relative, AnyElement, AsyncWindowContext, AvailableSpace,
+    Bounds, DispatchPhase, Element, ElementId, ExternalPaths, FocusHandle, Font, FontStyle,
+    FontWeight, HighlightStyle, Hsla, InteractiveElement, InteractiveElementState, Interactivity,
     IntoElement, LayoutId, Model, ModelContext, ModifiersChangedEvent, MouseButton, Pixels,
     PlatformInputHandler, Point, Rgba, ShapedLine, Size, StatefulInteractiveElement, Styled,
     TextRun, TextStyle, TextSystem, UnderlineStyle, WhiteSpace, WindowContext,
@@ -133,13 +133,7 @@ impl LayoutRect {
         )
         .into();
 
-        cx.paint_quad(
-            Bounds::new(position, size),
-            Default::default(),
-            self.color,
-            Default::default(),
-            transparent_black(),
-        );
+        cx.paint_quad(fill(Bounds::new(position, size), self.color));
     }
 }
 
@@ -151,11 +145,11 @@ pub struct TerminalElement {
     focused: bool,
     cursor_visible: bool,
     can_navigate_to_selected_word: bool,
-    interactivity: gpui::Interactivity,
+    interactivity: Interactivity,
 }
 
 impl InteractiveElement for TerminalElement {
-    fn interactivity(&mut self) -> &mut gpui::Interactivity {
+    fn interactivity(&mut self) -> &mut Interactivity {
         &mut self.interactivity
     }
 }
@@ -611,141 +605,156 @@ impl TerminalElement {
     }
 
     fn register_mouse_listeners(
-        self,
+        &mut self,
         origin: Point<Pixels>,
         mode: TermMode,
         bounds: Bounds<Pixels>,
         cx: &mut WindowContext,
-    ) -> Self {
+    ) {
         let focus = self.focus.clone();
-        let connection = self.terminal.clone();
-
-        let mut this = self
-            .on_mouse_down(MouseButton::Left, {
-                let connection = connection.clone();
-                let focus = focus.clone();
-                move |e, cx| {
-                    cx.focus(&focus);
-                    //todo!(context menu)
-                    // v.context_menu.update(cx, |menu, _cx| menu.delay_cancel());
-                    connection.update(cx, |terminal, cx| {
-                        terminal.mouse_down(&e, origin);
+        let terminal = self.terminal.clone();
+
+        self.interactivity.on_mouse_down(MouseButton::Left, {
+            let terminal = terminal.clone();
+            let focus = focus.clone();
+            move |e, cx| {
+                cx.focus(&focus);
+                //todo!(context menu)
+                // v.context_menu.update(cx, |menu, _cx| menu.delay_cancel());
+                terminal.update(cx, |terminal, cx| {
+                    terminal.mouse_down(&e, origin);
 
+                    cx.notify();
+                })
+            }
+        });
+        self.interactivity.on_mouse_move({
+            let terminal = terminal.clone();
+            let focus = focus.clone();
+            move |e, cx| {
+                if e.pressed_button.is_some() && focus.is_focused(cx) && !cx.has_active_drag() {
+                    terminal.update(cx, |terminal, cx| {
+                        terminal.mouse_drag(e, origin, bounds);
                         cx.notify();
                     })
                 }
-            })
-            .on_mouse_move({
-                let connection = connection.clone();
-                let focus = focus.clone();
-                move |e, cx| {
-                    if e.pressed_button.is_some() && focus.is_focused(cx) && !cx.has_active_drag() {
-                        connection.update(cx, |terminal, cx| {
-                            terminal.mouse_drag(e, origin, bounds);
-                            cx.notify();
-                        })
-                    }
-                }
-            })
-            .on_mouse_up(
-                MouseButton::Left,
-                TerminalElement::generic_button_handler(
-                    connection.clone(),
-                    origin,
-                    focus.clone(),
-                    move |terminal, origin, e, cx| {
-                        terminal.mouse_up(&e, origin, cx);
-                    },
-                ),
-            )
-            .on_click({
-                let connection = connection.clone();
-                move |e, cx| {
-                    if e.down.button == MouseButton::Right {
-                        let mouse_mode = connection.update(cx, |terminal, _cx| {
-                            terminal.mouse_mode(e.down.modifiers.shift)
-                        });
-
-                        if !mouse_mode {
-                            //todo!(context menu)
-                            // view.deploy_context_menu(e.position, cx);
-                        }
-                    }
-                }
-            })
-            .on_mouse_move({
-                let connection = connection.clone();
-                let focus = focus.clone();
-                move |e, cx| {
-                    if focus.is_focused(cx) {
-                        connection.update(cx, |terminal, cx| {
-                            terminal.mouse_move(&e, origin);
-                            cx.notify();
-                        })
+            }
+        });
+        self.interactivity.on_mouse_up(
+            MouseButton::Left,
+            TerminalElement::generic_button_handler(
+                terminal.clone(),
+                origin,
+                focus.clone(),
+                move |terminal, origin, e, cx| {
+                    terminal.mouse_up(&e, origin, cx);
+                },
+            ),
+        );
+        self.interactivity.on_click({
+            let terminal = terminal.clone();
+            move |e, cx| {
+                if e.down.button == MouseButton::Right {
+                    let mouse_mode = terminal.update(cx, |terminal, _cx| {
+                        terminal.mouse_mode(e.down.modifiers.shift)
+                    });
+
+                    if !mouse_mode {
+                        //todo!(context menu)
+                        // view.deploy_context_menu(e.position, cx);
                     }
                 }
-            })
-            .on_scroll_wheel({
-                let connection = connection.clone();
-                move |e, cx| {
-                    connection.update(cx, |terminal, cx| {
-                        terminal.scroll_wheel(e, origin);
+            }
+        });
+
+        self.interactivity.on_mouse_move({
+            let terminal = terminal.clone();
+            let focus = focus.clone();
+            move |e, cx| {
+                if focus.is_focused(cx) {
+                    terminal.update(cx, |terminal, cx| {
+                        terminal.mouse_move(&e, origin);
                         cx.notify();
                     })
                 }
-            });
+            }
+        });
+        self.interactivity.on_scroll_wheel({
+            let terminal = terminal.clone();
+            move |e, cx| {
+                terminal.update(cx, |terminal, cx| {
+                    terminal.scroll_wheel(e, origin);
+                    cx.notify();
+                })
+            }
+        });
+
+        self.interactivity.on_drop::<ExternalPaths>({
+            let focus = focus.clone();
+            let terminal = terminal.clone();
+            move |external_paths, cx| {
+                cx.focus(&focus);
+                let mut new_text = external_paths
+                    .paths()
+                    .iter()
+                    .map(|path| format!(" {path:?}"))
+                    .join("");
+                new_text.push(' ');
+                terminal.update(cx, |terminal, _| {
+                    // todo!() long paths are not displayed properly albeit the text is there
+                    terminal.paste(&new_text);
+                });
+            }
+        });
 
         // Mouse mode handlers:
         // All mouse modes need the extra click handlers
         if mode.intersects(TermMode::MOUSE_MODE) {
-            this = this
-                .on_mouse_down(
-                    MouseButton::Right,
-                    TerminalElement::generic_button_handler(
-                        connection.clone(),
-                        origin,
-                        focus.clone(),
-                        move |terminal, origin, e, _cx| {
-                            terminal.mouse_down(&e, origin);
-                        },
-                    ),
-                )
-                .on_mouse_down(
-                    MouseButton::Middle,
-                    TerminalElement::generic_button_handler(
-                        connection.clone(),
-                        origin,
-                        focus.clone(),
-                        move |terminal, origin, e, _cx| {
-                            terminal.mouse_down(&e, origin);
-                        },
-                    ),
-                )
-                .on_mouse_up(
-                    MouseButton::Right,
-                    TerminalElement::generic_button_handler(
-                        connection.clone(),
-                        origin,
-                        focus.clone(),
-                        move |terminal, origin, e, cx| {
-                            terminal.mouse_up(&e, origin, cx);
-                        },
-                    ),
-                )
-                .on_mouse_up(
-                    MouseButton::Middle,
-                    TerminalElement::generic_button_handler(
-                        connection,
-                        origin,
-                        focus,
-                        move |terminal, origin, e, cx| {
-                            terminal.mouse_up(&e, origin, cx);
-                        },
-                    ),
-                )
+            self.interactivity.on_mouse_down(
+                MouseButton::Right,
+                TerminalElement::generic_button_handler(
+                    terminal.clone(),
+                    origin,
+                    focus.clone(),
+                    move |terminal, origin, e, _cx| {
+                        terminal.mouse_down(&e, origin);
+                    },
+                ),
+            );
+            self.interactivity.on_mouse_down(
+                MouseButton::Middle,
+                TerminalElement::generic_button_handler(
+                    terminal.clone(),
+                    origin,
+                    focus.clone(),
+                    move |terminal, origin, e, _cx| {
+                        terminal.mouse_down(&e, origin);
+                    },
+                ),
+            );
+            self.interactivity.on_mouse_up(
+                MouseButton::Right,
+                TerminalElement::generic_button_handler(
+                    terminal.clone(),
+                    origin,
+                    focus.clone(),
+                    move |terminal, origin, e, cx| {
+                        terminal.mouse_up(&e, origin, cx);
+                    },
+                ),
+            );
+            self.interactivity.on_mouse_up(
+                MouseButton::Middle,
+                TerminalElement::generic_button_handler(
+                    terminal,
+                    origin,
+                    focus,
+                    move |terminal, origin, e, cx| {
+                        terminal.mouse_up(&e, origin, cx);
+                    },
+                ),
+            );
         }
-
-        this
     }
 }
 
@@ -770,18 +779,17 @@ impl Element for TerminalElement {
         (layout_id, interactive_state)
     }
 
-    fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext<'_>) {
+    fn paint(
+        &mut self,
+        bounds: Bounds<Pixels>,
+        state: &mut Self::State,
+        cx: &mut WindowContext<'_>,
+    ) {
         let mut layout = self.compute_layout(bounds, cx);
 
         let theme = cx.theme();
 
-        cx.paint_quad(
-            bounds,
-            Default::default(),
-            layout.background_color,
-            Default::default(),
-            Hsla::default(),
-        );
+        cx.paint_quad(fill(bounds, layout.background_color));
         let origin = bounds.origin + Point::new(layout.gutter, px(0.));
 
         let terminal_input_handler = TerminalInputHandler {
@@ -795,33 +803,19 @@ impl Element for TerminalElement {
 
         let terminal_focus_handle = self.focus.clone();
         let terminal_handle = self.terminal.clone();
-        let mut this: TerminalElement = self
-            .register_mouse_listeners(origin, layout.mode, bounds, cx)
-            .drag_over::<ExternalPaths>(|style| {
-                // todo!() why does not it work? z-index of elements?
-                style.bg(cx.theme().colors().ghost_element_hover)
-            })
-            .on_drop::<ExternalPaths>(move |external_paths, cx| {
-                cx.focus(&terminal_focus_handle);
-                let mut new_text = external_paths
-                    .read(cx)
-                    .paths()
-                    .iter()
-                    .map(|path| format!(" {path:?}"))
-                    .join("");
-                new_text.push(' ');
-                terminal_handle.update(cx, |terminal, _| {
-                    // todo!() long paths are not displayed properly albeit the text is there
-                    terminal.paste(&new_text);
-                });
-            });
+        self.register_mouse_listeners(origin, layout.mode, bounds, cx);
 
-        let interactivity = mem::take(&mut this.interactivity);
+        // todo!(change this to work in terms of on_drag_move or some such)
+        // .drag_over::<ExternalPaths>(|style| {
+        //     // todo!() why does not it work? z-index of elements?
+        //     style.bg(cx.theme().colors().ghost_element_hover)
+        // })
 
+        let mut interactivity = mem::take(&mut self.interactivity);
         interactivity.paint(bounds, bounds.size, state, cx, |_, _, cx| {
-            cx.handle_input(&this.focus, terminal_input_handler);
+            cx.handle_input(&self.focus, terminal_input_handler);
 
-            this.register_key_listeners(cx);
+            self.register_key_listeners(cx);
 
             for rect in &layout.rects {
                 rect.paint(origin, &layout, cx);
@@ -852,7 +846,7 @@ impl Element for TerminalElement {
                 }
             });
 
-            if this.cursor_visible {
+            if self.cursor_visible {
                 cx.with_z_index(3, |cx| {
                     if let Some(cursor) = &layout.cursor {
                         cursor.paint(origin, cx);
@@ -860,7 +854,7 @@ impl Element for TerminalElement {
                 });
             }
 
-            if let Some(element) = layout.hyperlink_tooltip.take() {
+            if let Some(mut element) = layout.hyperlink_tooltip.take() {
                 let width: AvailableSpace = bounds.size.width.into();
                 let height: AvailableSpace = bounds.size.height.into();
                 element.draw(origin, Size { width, height }, cx)

crates/theme2/src/themes/andromeda.rs πŸ”—

@@ -47,6 +47,7 @@ pub fn andromeda() -> UserThemeFamily {
                         scrollbar_thumb_border: Some(rgba(0x3a3f4c77).into()),
                         scrollbar_track_background: Some(rgba(0x23262eff).into()),
                         scrollbar_track_border: Some(rgba(0x1b1d23ff).into()),
+                        editor_foreground: Some(rgba(0xd5ced9ff).into()),
                         editor_background: Some(rgba(0x23262eff).into()),
                         editor_gutter_background: Some(rgba(0x23262eff).into()),
                         editor_line_number: Some(rgba(0x746f77ff).into()),
@@ -261,6 +262,7 @@ pub fn andromeda() -> UserThemeFamily {
                         scrollbar_thumb_border: Some(rgba(0x3a3f4c77).into()),
                         scrollbar_track_background: Some(rgba(0x262a33ff).into()),
                         scrollbar_track_border: Some(rgba(0x1b1d23ff).into()),
+                        editor_foreground: Some(rgba(0xd5ced9ff).into()),
                         editor_background: Some(rgba(0x262a33ff).into()),
                         editor_gutter_background: Some(rgba(0x262a33ff).into()),
                         editor_line_number: Some(rgba(0x746f77ff).into()),

crates/theme2/src/themes/ayu.rs πŸ”—

@@ -46,6 +46,7 @@ pub fn ayu() -> UserThemeFamily {
                         scrollbar_thumb_border: Some(rgba(0x8a919966).into()),
                         scrollbar_track_background: Some(rgba(0xf8f9faff).into()),
                         scrollbar_track_border: Some(rgba(0x6b7d8f1f).into()),
+                        editor_foreground: Some(rgba(0x5c6166ff).into()),
                         editor_background: Some(rgba(0xf8f9faff).into()),
                         editor_gutter_background: Some(rgba(0xf8f9faff).into()),
                         editor_line_number: Some(rgba(0x8a919966).into()),
@@ -352,6 +353,7 @@ pub fn ayu() -> UserThemeFamily {
                         scrollbar_thumb_border: Some(rgba(0x707a8c66).into()),
                         scrollbar_track_background: Some(rgba(0x1f2430ff).into()),
                         scrollbar_track_border: Some(rgba(0x171b24ff).into()),
+                        editor_foreground: Some(rgba(0xcccac2ff).into()),
                         editor_background: Some(rgba(0x1f2430ff).into()),
                         editor_gutter_background: Some(rgba(0x1f2430ff).into()),
                         editor_line_number: Some(rgba(0x8a919966).into()),
@@ -658,6 +660,7 @@ pub fn ayu() -> UserThemeFamily {
                         scrollbar_thumb_border: Some(rgba(0x565b6666).into()),
                         scrollbar_track_background: Some(rgba(0x0b0e14ff).into()),
                         scrollbar_track_border: Some(rgba(0x1e232bff).into()),
+                        editor_foreground: Some(rgba(0xbfbdb6ff).into()),
                         editor_background: Some(rgba(0x0b0e14ff).into()),
                         editor_gutter_background: Some(rgba(0x0b0e14ff).into()),
                         editor_line_number: Some(rgba(0x6c738099).into()),

crates/theme2/src/themes/dracula.rs πŸ”—

@@ -43,6 +43,7 @@ pub fn dracula() -> UserThemeFamily {
                     tab_active_background: Some(rgba(0x282a36ff).into()),
                     scrollbar_track_background: Some(rgba(0x282a36ff).into()),
                     scrollbar_track_border: Some(rgba(0x191a21ff).into()),
+                    editor_foreground: Some(rgba(0xf8f8f2ff).into()),
                     editor_background: Some(rgba(0x282a36ff).into()),
                     editor_gutter_background: Some(rgba(0x282a36ff).into()),
                     editor_line_number: Some(rgba(0x6272a4ff).into()),

crates/theme2/src/themes/gruvbox.rs πŸ”—

@@ -46,6 +46,7 @@ pub fn gruvbox() -> UserThemeFamily {
                         scrollbar_thumb_border: Some(rgba(0x50494599).into()),
                         scrollbar_track_background: Some(rgba(0x1d2021ff).into()),
                         scrollbar_track_border: Some(rgba(0x1d202100).into()),
+                        editor_foreground: Some(rgba(0xebdbb2ff).into()),
                         editor_background: Some(rgba(0x1d2021ff).into()),
                         editor_gutter_background: Some(rgba(0x1d2021ff).into()),
                         editor_line_number: Some(rgba(0x665c54ff).into()),
@@ -338,6 +339,7 @@ pub fn gruvbox() -> UserThemeFamily {
                         scrollbar_thumb_border: Some(rgba(0x50494599).into()),
                         scrollbar_track_background: Some(rgba(0x282828ff).into()),
                         scrollbar_track_border: Some(rgba(0x28282800).into()),
+                        editor_foreground: Some(rgba(0xebdbb2ff).into()),
                         editor_background: Some(rgba(0x282828ff).into()),
                         editor_gutter_background: Some(rgba(0x282828ff).into()),
                         editor_line_number: Some(rgba(0x665c54ff).into()),
@@ -630,6 +632,7 @@ pub fn gruvbox() -> UserThemeFamily {
                         scrollbar_thumb_border: Some(rgba(0x50494599).into()),
                         scrollbar_track_background: Some(rgba(0x32302fff).into()),
                         scrollbar_track_border: Some(rgba(0x32302f00).into()),
+                        editor_foreground: Some(rgba(0xebdbb2ff).into()),
                         editor_background: Some(rgba(0x32302fff).into()),
                         editor_gutter_background: Some(rgba(0x32302fff).into()),
                         editor_line_number: Some(rgba(0x665c54ff).into()),
@@ -922,6 +925,7 @@ pub fn gruvbox() -> UserThemeFamily {
                         scrollbar_thumb_border: Some(rgba(0xd5c4a199).into()),
                         scrollbar_track_background: Some(rgba(0xf9f5d7ff).into()),
                         scrollbar_track_border: Some(rgba(0xf9f5d700).into()),
+                        editor_foreground: Some(rgba(0x3c3836ff).into()),
                         editor_background: Some(rgba(0xf9f5d7ff).into()),
                         editor_gutter_background: Some(rgba(0xf9f5d7ff).into()),
                         editor_line_number: Some(rgba(0xbdae93ff).into()),
@@ -1214,6 +1218,7 @@ pub fn gruvbox() -> UserThemeFamily {
                         scrollbar_thumb_border: Some(rgba(0xd5c4a199).into()),
                         scrollbar_track_background: Some(rgba(0xfbf1c7ff).into()),
                         scrollbar_track_border: Some(rgba(0xfbf1c700).into()),
+                        editor_foreground: Some(rgba(0x3c3836ff).into()),
                         editor_background: Some(rgba(0xfbf1c7ff).into()),
                         editor_gutter_background: Some(rgba(0xfbf1c7ff).into()),
                         editor_line_number: Some(rgba(0xbdae93ff).into()),
@@ -1506,6 +1511,7 @@ pub fn gruvbox() -> UserThemeFamily {
                         scrollbar_thumb_border: Some(rgba(0xd5c4a199).into()),
                         scrollbar_track_background: Some(rgba(0xf2e5bcff).into()),
                         scrollbar_track_border: Some(rgba(0xf2e5bc00).into()),
+                        editor_foreground: Some(rgba(0x3c3836ff).into()),
                         editor_background: Some(rgba(0xf2e5bcff).into()),
                         editor_gutter_background: Some(rgba(0xf2e5bcff).into()),
                         editor_line_number: Some(rgba(0xbdae93ff).into()),

crates/theme2/src/themes/night_owl.rs πŸ”—

@@ -46,6 +46,7 @@ pub fn night_owl() -> UserThemeFamily {
                         scrollbar_thumb_hover_background: Some(rgba(0x084d8180).into()),
                         scrollbar_thumb_border: Some(rgba(0x084d8180).into()),
                         scrollbar_track_background: Some(rgba(0x011627ff).into()),
+                        editor_foreground: Some(rgba(0xd6deebff).into()),
                         editor_background: Some(rgba(0x011627ff).into()),
                         editor_gutter_background: Some(rgba(0x011627ff).into()),
                         editor_line_number: Some(rgba(0x4b6479ff).into()),
@@ -305,6 +306,7 @@ pub fn night_owl() -> UserThemeFamily {
                         tab_inactive_background: Some(rgba(0xf0f0f0ff).into()),
                         tab_active_background: Some(rgba(0xf6f6f6ff).into()),
                         scrollbar_track_background: Some(rgba(0xfbfbfbff).into()),
+                        editor_foreground: Some(rgba(0x403f53ff).into()),
                         editor_background: Some(rgba(0xfbfbfbff).into()),
                         editor_gutter_background: Some(rgba(0xfbfbfbff).into()),
                         editor_line_number: Some(rgba(0x90a7b2ff).into()),

crates/theme2/src/themes/noctis.rs πŸ”—

@@ -47,6 +47,7 @@ pub fn noctis() -> UserThemeFamily {
                         scrollbar_thumb_border: Some(rgba(0x008ee633).into()),
                         scrollbar_track_background: Some(rgba(0x07273bff).into()),
                         scrollbar_track_border: Some(rgba(0x07273bff).into()),
+                        editor_foreground: Some(rgba(0xbecfdaff).into()),
                         editor_background: Some(rgba(0x07273bff).into()),
                         editor_gutter_background: Some(rgba(0x07273bff).into()),
                         editor_line_number: Some(rgba(0x4d6c80ff).into()),
@@ -324,6 +325,7 @@ pub fn noctis() -> UserThemeFamily {
                         scrollbar_thumb_border: Some(rgba(0xeb609133).into()),
                         scrollbar_track_background: Some(rgba(0x322a2dff).into()),
                         scrollbar_track_border: Some(rgba(0x322a2dff).into()),
+                        editor_foreground: Some(rgba(0xcbbec2ff).into()),
                         editor_background: Some(rgba(0x322a2dff).into()),
                         editor_gutter_background: Some(rgba(0x322a2dff).into()),
                         editor_line_number: Some(rgba(0x715b63ff).into()),
@@ -601,6 +603,7 @@ pub fn noctis() -> UserThemeFamily {
                         scrollbar_thumb_border: Some(rgba(0x6a90955b).into()),
                         scrollbar_track_background: Some(rgba(0xf4f6f6ff).into()),
                         scrollbar_track_border: Some(rgba(0xf4f6f6ff).into()),
+                        editor_foreground: Some(rgba(0x005661ff).into()),
                         editor_background: Some(rgba(0xf4f6f6ff).into()),
                         editor_gutter_background: Some(rgba(0xf4f6f6ff).into()),
                         editor_line_number: Some(rgba(0xa0abacff).into()),
@@ -878,6 +881,7 @@ pub fn noctis() -> UserThemeFamily {
                         scrollbar_thumb_border: Some(rgba(0x6a90955b).into()),
                         scrollbar_track_background: Some(rgba(0xf2f1f8ff).into()),
                         scrollbar_track_border: Some(rgba(0xf2f1f8ff).into()),
+                        editor_foreground: Some(rgba(0x0c006bff).into()),
                         editor_background: Some(rgba(0xf2f1f8ff).into()),
                         editor_gutter_background: Some(rgba(0xf2f1f8ff).into()),
                         editor_line_number: Some(rgba(0x9d9ab1ff).into()),
@@ -1155,6 +1159,7 @@ pub fn noctis() -> UserThemeFamily {
                         scrollbar_thumb_border: Some(rgba(0x6a90955b).into()),
                         scrollbar_track_background: Some(rgba(0xfef8ecff).into()),
                         scrollbar_track_border: Some(rgba(0xfef8ecff).into()),
+                        editor_foreground: Some(rgba(0x005661ff).into()),
                         editor_background: Some(rgba(0xfef8ecff).into()),
                         editor_gutter_background: Some(rgba(0xfef8ecff).into()),
                         editor_line_number: Some(rgba(0xa0abacff).into()),
@@ -1432,6 +1437,7 @@ pub fn noctis() -> UserThemeFamily {
                         scrollbar_thumb_border: Some(rgba(0x3f7fa633).into()),
                         scrollbar_track_background: Some(rgba(0x1b2932ff).into()),
                         scrollbar_track_border: Some(rgba(0x1b2932ff).into()),
+                        editor_foreground: Some(rgba(0xc5cdd3ff).into()),
                         editor_background: Some(rgba(0x1b2932ff).into()),
                         editor_gutter_background: Some(rgba(0x1b2932ff).into()),
                         editor_line_number: Some(rgba(0x5d6e79ff).into()),
@@ -1709,6 +1715,7 @@ pub fn noctis() -> UserThemeFamily {
                         scrollbar_thumb_border: Some(rgba(0x6a90955b).into()),
                         scrollbar_track_background: Some(rgba(0x052529ff).into()),
                         scrollbar_track_border: Some(rgba(0x052529ff).into()),
+                        editor_foreground: Some(rgba(0xb2cacdff).into()),
                         editor_background: Some(rgba(0x052529ff).into()),
                         editor_gutter_background: Some(rgba(0x052529ff).into()),
                         editor_line_number: Some(rgba(0x4e6b6eff).into()),
@@ -1986,6 +1993,7 @@ pub fn noctis() -> UserThemeFamily {
                         scrollbar_thumb_border: Some(rgba(0x6a90955b).into()),
                         scrollbar_track_background: Some(rgba(0x031417ff).into()),
                         scrollbar_track_border: Some(rgba(0x031417ff).into()),
+                        editor_foreground: Some(rgba(0xb2cacdff).into()),
                         editor_background: Some(rgba(0x031417ff).into()),
                         editor_gutter_background: Some(rgba(0x031417ff).into()),
                         editor_line_number: Some(rgba(0x4e6b6eff).into()),
@@ -2263,6 +2271,7 @@ pub fn noctis() -> UserThemeFamily {
                         scrollbar_thumb_border: Some(rgba(0x6a90955b).into()),
                         scrollbar_track_background: Some(rgba(0x031417ff).into()),
                         scrollbar_track_border: Some(rgba(0x031417ff).into()),
+                        editor_foreground: Some(rgba(0xb2cacdff).into()),
                         editor_background: Some(rgba(0x031417ff).into()),
                         editor_gutter_background: Some(rgba(0x031417ff).into()),
                         editor_line_number: Some(rgba(0x4e6b6eff).into()),
@@ -2540,6 +2549,7 @@ pub fn noctis() -> UserThemeFamily {
                         scrollbar_thumb_border: Some(rgba(0x7060eb33).into()),
                         scrollbar_track_background: Some(rgba(0x292640ff).into()),
                         scrollbar_track_border: Some(rgba(0x292640ff).into()),
+                        editor_foreground: Some(rgba(0xc5c2d6ff).into()),
                         editor_background: Some(rgba(0x292640ff).into()),
                         editor_gutter_background: Some(rgba(0x292640ff).into()),
                         editor_line_number: Some(rgba(0x5c5973ff).into()),
@@ -2817,6 +2827,7 @@ pub fn noctis() -> UserThemeFamily {
                         scrollbar_thumb_border: Some(rgba(0xa660eb33).into()),
                         scrollbar_track_background: Some(rgba(0x30243dff).into()),
                         scrollbar_track_border: Some(rgba(0x30243dff).into()),
+                        editor_foreground: Some(rgba(0xccbfd9ff).into()),
                         editor_background: Some(rgba(0x30243dff).into()),
                         editor_gutter_background: Some(rgba(0x30243dff).into()),
                         editor_line_number: Some(rgba(0x665973ff).into()),

crates/theme2/src/themes/nord.rs πŸ”—

@@ -46,6 +46,7 @@ pub fn nord() -> UserThemeFamily {
                     scrollbar_thumb_border: Some(rgba(0x434c5e99).into()),
                     scrollbar_track_background: Some(rgba(0x2e3440ff).into()),
                     scrollbar_track_border: Some(rgba(0x3b4252ff).into()),
+                    editor_foreground: Some(rgba(0xd8dee9ff).into()),
                     editor_background: Some(rgba(0x2e3440ff).into()),
                     editor_gutter_background: Some(rgba(0x2e3440ff).into()),
                     editor_line_number: Some(rgba(0x4c566aff).into()),

crates/theme2/src/themes/palenight.rs πŸ”—

@@ -46,6 +46,7 @@ pub fn palenight() -> UserThemeFamily {
                         scrollbar_thumb_hover_background: Some(rgba(0x694ca4cc).into()),
                         scrollbar_thumb_border: Some(rgba(0x694ca466).into()),
                         scrollbar_track_background: Some(rgba(0x292d3eff).into()),
+                        editor_foreground: Some(rgba(0xbfc7d5ff).into()),
                         editor_background: Some(rgba(0x292d3eff).into()),
                         editor_gutter_background: Some(rgba(0x292d3eff).into()),
                         editor_line_number: Some(rgba(0x4c5374ff).into()),
@@ -331,6 +332,7 @@ pub fn palenight() -> UserThemeFamily {
                         scrollbar_thumb_hover_background: Some(rgba(0x694ca4cc).into()),
                         scrollbar_thumb_border: Some(rgba(0x694ca466).into()),
                         scrollbar_track_background: Some(rgba(0x292d3eff).into()),
+                        editor_foreground: Some(rgba(0xbfc7d5ff).into()),
                         editor_background: Some(rgba(0x292d3eff).into()),
                         editor_gutter_background: Some(rgba(0x292d3eff).into()),
                         editor_line_number: Some(rgba(0x4c5374ff).into()),
@@ -616,6 +618,7 @@ pub fn palenight() -> UserThemeFamily {
                         scrollbar_thumb_hover_background: Some(rgba(0x694ca4cc).into()),
                         scrollbar_thumb_border: Some(rgba(0x694ca466).into()),
                         scrollbar_track_background: Some(rgba(0x292d3eff).into()),
+                        editor_foreground: Some(rgba(0xbfc7d5ff).into()),
                         editor_background: Some(rgba(0x292d3eff).into()),
                         editor_gutter_background: Some(rgba(0x292d3eff).into()),
                         editor_line_number: Some(rgba(0x4c5374ff).into()),

crates/theme2/src/themes/rose_pine.rs πŸ”—

@@ -47,6 +47,7 @@ pub fn rose_pine() -> UserThemeFamily {
                         scrollbar_thumb_border: Some(rgba(0x6e6a8633).into()),
                         scrollbar_track_background: Some(rgba(0x191724ff).into()),
                         scrollbar_track_border: Some(rgba(0x6e6a8666).into()),
+                        editor_foreground: Some(rgba(0xe0def4ff).into()),
                         editor_background: Some(rgba(0x191724ff).into()),
                         editor_gutter_background: Some(rgba(0x191724ff).into()),
                         editor_line_number: Some(rgba(0x908caaff).into()),
@@ -306,6 +307,7 @@ pub fn rose_pine() -> UserThemeFamily {
                         scrollbar_thumb_border: Some(rgba(0x817c9c26).into()),
                         scrollbar_track_background: Some(rgba(0x232136ff).into()),
                         scrollbar_track_border: Some(rgba(0x817c9c4d).into()),
+                        editor_foreground: Some(rgba(0xe0def4ff).into()),
                         editor_background: Some(rgba(0x232136ff).into()),
                         editor_gutter_background: Some(rgba(0x232136ff).into()),
                         editor_line_number: Some(rgba(0x908caaff).into()),
@@ -565,6 +567,7 @@ pub fn rose_pine() -> UserThemeFamily {
                         scrollbar_thumb_border: Some(rgba(0x6e6a8614).into()),
                         scrollbar_track_background: Some(rgba(0xfaf4edff).into()),
                         scrollbar_track_border: Some(rgba(0x6e6a8626).into()),
+                        editor_foreground: Some(rgba(0x575279ff).into()),
                         editor_background: Some(rgba(0xfaf4edff).into()),
                         editor_gutter_background: Some(rgba(0xfaf4edff).into()),
                         editor_line_number: Some(rgba(0x797593ff).into()),

crates/theme2/src/themes/solarized.rs πŸ”—

@@ -42,6 +42,7 @@ pub fn solarized() -> UserThemeFamily {
                         tab_inactive_background: Some(rgba(0x004052ff).into()),
                         tab_active_background: Some(rgba(0x002b37ff).into()),
                         scrollbar_track_background: Some(rgba(0x002b36ff).into()),
+                        editor_foreground: Some(rgba(0xbbbbbbff).into()),
                         editor_background: Some(rgba(0x002b36ff).into()),
                         editor_gutter_background: Some(rgba(0x002b36ff).into()),
                         editor_line_number: Some(rgba(0x566c74ff).into()),
@@ -307,6 +308,7 @@ pub fn solarized() -> UserThemeFamily {
                         tab_inactive_background: Some(rgba(0xd3cbb7ff).into()),
                         tab_active_background: Some(rgba(0xfdf6e3ff).into()),
                         scrollbar_track_background: Some(rgba(0xfdf6e3ff).into()),
+                        editor_foreground: Some(rgba(0x333333ff).into()),
                         editor_background: Some(rgba(0xfdf6e3ff).into()),
                         editor_gutter_background: Some(rgba(0xfdf6e3ff).into()),
                         editor_line_number: Some(rgba(0x9ca8a6ff).into()),

crates/theme_importer/src/theme_printer.rs πŸ”—

@@ -201,6 +201,7 @@ impl<'a> Debug for ThemeColorsRefinementPrinter<'a> {
                 self.0.scrollbar_track_background,
             ),
             ("scrollbar_track_border", self.0.scrollbar_track_border),
+            ("editor_foreground", self.0.editor_foreground),
             ("editor_background", self.0.editor_background),
             ("editor_gutter_background", self.0.editor_gutter_background),
             (

crates/theme_importer/src/vscode/converter.rs πŸ”—

@@ -132,6 +132,11 @@ impl VsCodeThemeConverter {
             .as_ref()
             .traverse(|color| try_parse_color(&color))?;
 
+        let vscode_editor_foreground = vscode_colors
+            .editor_foreground
+            .as_ref()
+            .traverse(|color| try_parse_color(&color))?;
+
         let vscode_editor_background = vscode_colors
             .editor_background
             .as_ref()
@@ -142,6 +147,16 @@ impl VsCodeThemeConverter {
             .as_ref()
             .traverse(|color| try_parse_color(&color))?;
 
+        let vscode_token_colors_foreground = self
+            .theme
+            .token_colors
+            .iter()
+            .find(|token_color| token_color.scope.is_none())
+            .and_then(|token_color| token_color.settings.foreground.as_ref())
+            .traverse(|color| try_parse_color(&color))
+            .ok()
+            .flatten();
+
         Ok(ThemeColorsRefinement {
             border: vscode_panel_border,
             border_variant: vscode_panel_border,
@@ -197,16 +212,7 @@ impl VsCodeThemeConverter {
                 .foreground
                 .as_ref()
                 .traverse(|color| try_parse_color(&color))?
-                .or_else(|| {
-                    self.theme
-                        .token_colors
-                        .iter()
-                        .find(|token_color| token_color.scope.is_none())
-                        .and_then(|token_color| token_color.settings.foreground.as_ref())
-                        .traverse(|color| try_parse_color(&color))
-                        .ok()
-                        .flatten()
-                }),
+                .or(vscode_token_colors_foreground),
             text_muted: vscode_colors
                 .tab_inactive_foreground
                 .as_ref()
@@ -226,6 +232,7 @@ impl VsCodeThemeConverter {
                 .as_ref()
                 .traverse(|color| try_parse_color(&color))?
                 .or(vscode_editor_background),
+            editor_foreground: vscode_editor_foreground.or(vscode_token_colors_foreground),
             editor_background: vscode_editor_background,
             editor_gutter_background: vscode_editor_background,
             editor_line_number: vscode_colors

crates/ui2/src/components/button/button_like.rs πŸ”—

@@ -2,7 +2,6 @@ use gpui::{relative, DefiniteLength};
 use gpui::{rems, transparent_black, AnyElement, AnyView, ClickEvent, Div, Hsla, Rems, Stateful};
 use smallvec::SmallVec;
 
-use crate::h_stack;
 use crate::prelude::*;
 
 pub trait ButtonCommon: Clickable + Disableable {
@@ -250,6 +249,7 @@ impl ButtonSize {
 /// This is also used to build the prebuilt buttons.
 #[derive(IntoElement)]
 pub struct ButtonLike {
+    base: Div,
     id: ElementId,
     pub(super) style: ButtonStyle,
     pub(super) disabled: bool,
@@ -264,6 +264,7 @@ pub struct ButtonLike {
 impl ButtonLike {
     pub fn new(id: impl Into<ElementId>) -> Self {
         Self {
+            base: div(),
             id: id.into(),
             style: ButtonStyle::default(),
             disabled: false,
@@ -331,6 +332,13 @@ impl ButtonCommon for ButtonLike {
     }
 }
 
+impl VisibleOnHover for ButtonLike {
+    fn visible_on_hover(mut self, group_name: impl Into<SharedString>) -> Self {
+        self.base = self.base.visible_on_hover(group_name);
+        self
+    }
+}
+
 impl ParentElement for ButtonLike {
     fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
         &mut self.children
@@ -341,7 +349,8 @@ impl RenderOnce for ButtonLike {
     type Rendered = Stateful<Div>;
 
     fn render(self, cx: &mut WindowContext) -> Self::Rendered {
-        h_stack()
+        self.base
+            .h_flex()
             .id(self.id.clone())
             .group("")
             .flex_none()

crates/ui2/src/components/button/icon_button.rs πŸ”—

@@ -98,6 +98,13 @@ impl ButtonCommon for IconButton {
     }
 }
 
+impl VisibleOnHover for IconButton {
+    fn visible_on_hover(mut self, group_name: impl Into<SharedString>) -> Self {
+        self.base = self.base.visible_on_hover(group_name);
+        self
+    }
+}
+
 impl RenderOnce for IconButton {
     type Rendered = ButtonLike;
 

crates/ui2/src/components/disclosure.rs πŸ”—

@@ -1,6 +1,7 @@
-use crate::{prelude::*, Color, Icon, IconButton, IconSize};
 use gpui::ClickEvent;
 
+use crate::{prelude::*, Color, Icon, IconButton, IconSize};
+
 #[derive(IntoElement)]
 pub struct Disclosure {
     is_open: bool,

crates/ui2/src/components/list/list_header.rs πŸ”—

@@ -111,10 +111,9 @@ impl RenderOnce for ListHeader {
                 .when_some(self.end_hover_slot, |this, end_hover_slot| {
                     this.child(
                         div()
-                            .invisible()
-                            .group_hover("list_header", |this| this.visible())
                             .absolute()
                             .right_0()
+                            .visible_on_hover("list_header")
                             .child(end_hover_slot),
                     )
                 }),

crates/ui2/src/components/list/list_item.rs πŸ”—

@@ -1,9 +1,10 @@
-use crate::{prelude::*, Disclosure};
 use gpui::{
     px, AnyElement, AnyView, ClickEvent, Div, MouseButton, MouseDownEvent, Pixels, Stateful,
 };
 use smallvec::SmallVec;
 
+use crate::{prelude::*, Disclosure};
+
 #[derive(IntoElement)]
 pub struct ListItem {
     id: ElementId,
@@ -127,7 +128,7 @@ impl RenderOnce for ListItem {
 
     fn render(self, cx: &mut WindowContext) -> Self::Rendered {
         h_stack()
-            .id("item_container")
+            .id(self.id)
             .w_full()
             .relative()
             // When an item is inset draw the indent spacing outside of the item
@@ -150,7 +151,7 @@ impl RenderOnce for ListItem {
             })
             .child(
                 h_stack()
-                    .id(self.id)
+                    .id("inner_list_item")
                     .w_full()
                     .relative()
                     .gap_1()
@@ -170,13 +171,7 @@ impl RenderOnce for ListItem {
                             })
                     })
                     .when_some(self.on_click, |this, on_click| {
-                        this.cursor_pointer().on_click(move |event, cx| {
-                            // HACK: GPUI currently fires `on_click` with any mouse button,
-                            // but we only care about the left button.
-                            if event.down.button == MouseButton::Left {
-                                (on_click)(event, cx)
-                            }
-                        })
+                        this.cursor_pointer().on_click(on_click)
                     })
                     .when_some(self.on_secondary_mouse_down, |this, on_mouse_down| {
                         this.on_mouse_down(MouseButton::Right, move |event, cx| {
@@ -192,10 +187,14 @@ impl RenderOnce for ListItem {
                             this.ml(self.indent_level as f32 * self.indent_step_size)
                         }
                     })
-                    .children(
-                        self.toggle
-                            .map(|is_open| Disclosure::new(is_open).on_toggle(self.on_toggle)),
-                    )
+                    .children(self.toggle.map(|is_open| {
+                        div()
+                            .flex()
+                            .absolute()
+                            .left(rems(-1.))
+                            .visible_on_hover("")
+                            .child(Disclosure::new(is_open).on_toggle(self.on_toggle))
+                    }))
                     .child(
                         h_stack()
                             .flex_1()
@@ -220,8 +219,7 @@ impl RenderOnce for ListItem {
                                 .absolute()
                                 .right_2()
                                 .top_0()
-                                .invisible()
-                                .group_hover("list_item", |this| this.visible())
+                                .visible_on_hover("list_item")
                                 .child(end_hover_slot),
                         )
                     }),

crates/ui2/src/components/popover_menu.rs πŸ”—

@@ -182,12 +182,12 @@ impl<M: ManagedView> Element for PopoverMenu<M> {
     }
 
     fn paint(
-        self,
+        &mut self,
         _: Bounds<gpui::Pixels>,
         element_state: &mut Self::State,
         cx: &mut WindowContext,
     ) {
-        if let Some(child) = element_state.child_element.take() {
+        if let Some(mut child) = element_state.child_element.take() {
             child.paint(cx);
         }
 
@@ -195,7 +195,7 @@ impl<M: ManagedView> Element for PopoverMenu<M> {
             element_state.child_bounds = Some(cx.layout_bounds(child_layout_id));
         }
 
-        if let Some(menu) = element_state.menu_element.take() {
+        if let Some(mut menu) = element_state.menu_element.take() {
             menu.paint(cx);
 
             if let Some(child_bounds) = element_state.child_bounds {

crates/ui2/src/components/right_click_menu.rs πŸ”—

@@ -112,21 +112,21 @@ impl<M: ManagedView> Element for RightClickMenu<M> {
     }
 
     fn paint(
-        self,
+        &mut self,
         bounds: Bounds<gpui::Pixels>,
         element_state: &mut Self::State,
         cx: &mut WindowContext,
     ) {
-        if let Some(child) = element_state.child_element.take() {
+        if let Some(mut child) = element_state.child_element.take() {
             child.paint(cx);
         }
 
-        if let Some(menu) = element_state.menu_element.take() {
+        if let Some(mut menu) = element_state.menu_element.take() {
             menu.paint(cx);
             return;
         }
 
-        let Some(builder) = self.menu_builder else {
+        let Some(builder) = self.menu_builder.take() else {
             return;
         };
         let menu = element_state.menu.clone();

crates/ui2/src/components/stories/list_item.rs πŸ”—

@@ -17,7 +17,7 @@ impl Render for ListItemStory {
             .child(ListItem::new("hello_world").child("Hello, world!"))
             .child(Story::label("Inset"))
             .child(
-                ListItem::new("hello_world")
+                ListItem::new("inset_list_item")
                     .inset(true)
                     .start_slot(
                         IconElement::new(Icon::Bell)
@@ -59,7 +59,7 @@ impl Render for ListItemStory {
             )
             .child(Story::label("With end hover slot"))
             .child(
-                ListItem::new("with_left_avatar")
+                ListItem::new("with_end_hover_slot")
                     .child("Hello, world!")
                     .end_slot(
                         h_stack()

crates/ui2/src/components/tab.rs πŸ”—

@@ -158,7 +158,6 @@ impl RenderOnce for Tab {
                     )
                     .child(
                         h_stack()
-                            .invisible()
                             .w_3()
                             .h_3()
                             .justify_center()
@@ -167,7 +166,7 @@ impl RenderOnce for Tab {
                                 TabCloseSide::Start => this.left_1(),
                                 TabCloseSide::End => this.right_1(),
                             })
-                            .group_hover("", |style| style.visible())
+                            .visible_on_hover("")
                             .children(self.end_slot),
                     )
                     .children(self.children),

crates/ui2/src/prelude.rs πŸ”—

@@ -9,6 +9,7 @@ pub use crate::clickable::*;
 pub use crate::disableable::*;
 pub use crate::fixed::*;
 pub use crate::selectable::*;
+pub use crate::visible_on_hover::*;
 pub use crate::{h_stack, v_stack};
 pub use crate::{Button, ButtonSize, ButtonStyle, IconButton};
 pub use crate::{ButtonCommon, Color, StyledExt};

crates/ui2/src/styles/elevation.rs πŸ”—

@@ -20,14 +20,14 @@ pub enum ElevationIndex {
 }
 
 impl ElevationIndex {
-    pub fn z_index(self) -> u32 {
+    pub fn z_index(self) -> u8 {
         match self {
             ElevationIndex::Background => 0,
-            ElevationIndex::Surface => 100,
-            ElevationIndex::ElevatedSurface => 200,
-            ElevationIndex::Wash => 250,
-            ElevationIndex::ModalSurface => 300,
-            ElevationIndex::DraggedElement => 900,
+            ElevationIndex::Surface => 42,
+            ElevationIndex::ElevatedSurface => 84,
+            ElevationIndex::Wash => 126,
+            ElevationIndex::ModalSurface => 168,
+            ElevationIndex::DraggedElement => 210,
         }
     }
 

crates/ui2/src/ui2.rs πŸ”—

@@ -21,6 +21,7 @@ mod selectable;
 mod styled_ext;
 mod styles;
 pub mod utils;
+mod visible_on_hover;
 
 pub use clickable::*;
 pub use components::*;
@@ -30,3 +31,4 @@ pub use prelude::*;
 pub use selectable::*;
 pub use styled_ext::*;
 pub use styles::*;
+pub use visible_on_hover::*;

crates/ui2/src/visible_on_hover.rs πŸ”—

@@ -0,0 +1,15 @@
+use gpui::{InteractiveElement, SharedString, Styled};
+
+pub trait VisibleOnHover {
+    /// Sets the element to only be visible when the specified group is hovered.
+    ///
+    /// Pass `""` as the `group_name` to use the global group.
+    fn visible_on_hover(self, group_name: impl Into<SharedString>) -> Self;
+}
+
+impl<E: InteractiveElement + Styled> VisibleOnHover for E {
+    fn visible_on_hover(self, group_name: impl Into<SharedString>) -> Self {
+        self.invisible()
+            .group_hover(group_name, |style| style.visible())
+    }
+}

crates/vcs_menu2/Cargo.toml πŸ”—

@@ -0,0 +1,17 @@
+[package]
+name = "vcs_menu2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+fuzzy = {package = "fuzzy2", path = "../fuzzy2"}
+fs = {package = "fs2", path = "../fs2"}
+gpui = {package = "gpui2", path = "../gpui2"}
+picker = {package = "picker2", path = "../picker2"}
+util = {path = "../util"}
+ui = {package = "ui2", path = "../ui2"}
+workspace = {package = "workspace2", path = "../workspace2"}
+
+anyhow.workspace = true

crates/vcs_menu2/src/lib.rs πŸ”—

@@ -0,0 +1,359 @@
+use anyhow::{anyhow, bail, Result};
+use fs::repository::Branch;
+use fuzzy::{StringMatch, StringMatchCandidate};
+use gpui::{
+    actions, rems, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView,
+    ParentElement, Render, SharedString, Styled, Task, View, ViewContext, VisualContext,
+    WindowContext,
+};
+use picker::{Picker, PickerDelegate};
+use std::sync::Arc;
+use ui::{v_stack, HighlightedLabel, ListItem, Selectable};
+use util::ResultExt;
+use workspace::{ModalView, Toast, Workspace};
+
+actions!(branches, [OpenRecent]);
+
+pub fn init(cx: &mut AppContext) {
+    // todo!() po
+    cx.observe_new_views(|workspace: &mut Workspace, _| {
+        workspace.register_action(|workspace, action, cx| {
+            ModalBranchList::toggle_modal(workspace, action, cx).log_err();
+        });
+    })
+    .detach();
+}
+pub type BranchList = Picker<BranchListDelegate>;
+
+pub struct ModalBranchList {
+    pub picker: View<Picker<BranchListDelegate>>,
+}
+
+impl ModalView for ModalBranchList {}
+impl EventEmitter<DismissEvent> for ModalBranchList {}
+
+impl FocusableView for ModalBranchList {
+    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+        self.picker.focus_handle(cx)
+    }
+}
+
+impl Render for ModalBranchList {
+    type Element = Div;
+
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
+        v_stack().w(rems(34.)).child(self.picker.clone())
+    }
+}
+
+pub fn build_branch_list(
+    workspace: View<Workspace>,
+    cx: &mut WindowContext<'_>,
+) -> Result<View<BranchList>> {
+    let delegate = workspace.update(cx, |workspace, cx| {
+        BranchListDelegate::new(workspace, cx.view().clone(), 29, cx)
+    })?;
+
+    Ok(cx.build_view(|cx| Picker::new(delegate, cx)))
+}
+
+impl ModalBranchList {
+    fn toggle_modal(
+        workspace: &mut Workspace,
+        _: &OpenRecent,
+        cx: &mut ViewContext<Workspace>,
+    ) -> Result<()> {
+        // Modal branch picker has a longer trailoff than a popover one.
+        let delegate = BranchListDelegate::new(workspace, cx.view().clone(), 70, cx)?;
+        workspace.toggle_modal(cx, |cx| ModalBranchList {
+            picker: cx.build_view(|cx| Picker::new(delegate, cx)),
+        });
+
+        Ok(())
+    }
+}
+
+pub struct BranchListDelegate {
+    matches: Vec<StringMatch>,
+    all_branches: Vec<Branch>,
+    workspace: View<Workspace>,
+    selected_index: usize,
+    last_query: String,
+    /// Max length of branch name before we truncate it and add a trailing `...`.
+    branch_name_trailoff_after: usize,
+}
+
+impl BranchListDelegate {
+    fn new(
+        workspace: &Workspace,
+        handle: View<Workspace>,
+        branch_name_trailoff_after: usize,
+        cx: &AppContext,
+    ) -> Result<Self> {
+        let project = workspace.project().read(&cx);
+        let Some(worktree) = project.visible_worktrees(cx).next() else {
+            bail!("Cannot update branch list as there are no visible worktrees")
+        };
+
+        let mut cwd = worktree.read(cx).abs_path().to_path_buf();
+        cwd.push(".git");
+        let Some(repo) = project.fs().open_repo(&cwd) else {
+            bail!("Project does not have associated git repository.")
+        };
+        let all_branches = repo.lock().branches()?;
+        Ok(Self {
+            matches: vec![],
+            workspace: handle,
+            all_branches,
+            selected_index: 0,
+            last_query: Default::default(),
+            branch_name_trailoff_after,
+        })
+    }
+
+    fn display_error_toast(&self, message: String, cx: &mut ViewContext<BranchList>) {
+        const GIT_CHECKOUT_FAILURE_ID: usize = 2048;
+        self.workspace.update(cx, |model, ctx| {
+            model.show_toast(Toast::new(GIT_CHECKOUT_FAILURE_ID, message), ctx)
+        });
+    }
+}
+
+impl PickerDelegate for BranchListDelegate {
+    type ListItem = ListItem;
+    fn placeholder_text(&self) -> Arc<str> {
+        "Select branch...".into()
+    }
+
+    fn match_count(&self) -> usize {
+        self.matches.len()
+    }
+
+    fn selected_index(&self) -> usize {
+        self.selected_index
+    }
+
+    fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
+        self.selected_index = ix;
+    }
+
+    fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
+        cx.spawn(move |picker, mut cx| async move {
+            let candidates = picker.update(&mut cx, |view, _| {
+                const RECENT_BRANCHES_COUNT: usize = 10;
+                let mut branches = view.delegate.all_branches.clone();
+                if query.is_empty() && branches.len() > RECENT_BRANCHES_COUNT {
+                    // Truncate list of recent branches
+                    // Do a partial sort to show recent-ish branches first.
+                    branches.select_nth_unstable_by(RECENT_BRANCHES_COUNT - 1, |lhs, rhs| {
+                        rhs.unix_timestamp.cmp(&lhs.unix_timestamp)
+                    });
+                    branches.truncate(RECENT_BRANCHES_COUNT);
+                    branches.sort_unstable_by(|lhs, rhs| lhs.name.cmp(&rhs.name));
+                }
+                branches
+                    .into_iter()
+                    .enumerate()
+                    .map(|(ix, command)| StringMatchCandidate {
+                        id: ix,
+                        char_bag: command.name.chars().collect(),
+                        string: command.name.into(),
+                    })
+                    .collect::<Vec<StringMatchCandidate>>()
+            });
+            let Some(candidates) = candidates.log_err() else {
+                return;
+            };
+            let matches = if query.is_empty() {
+                candidates
+                    .into_iter()
+                    .enumerate()
+                    .map(|(index, candidate)| StringMatch {
+                        candidate_id: index,
+                        string: candidate.string,
+                        positions: Vec::new(),
+                        score: 0.0,
+                    })
+                    .collect()
+            } else {
+                fuzzy::match_strings(
+                    &candidates,
+                    &query,
+                    true,
+                    10000,
+                    &Default::default(),
+                    cx.background_executor().clone(),
+                )
+                .await
+            };
+            picker
+                .update(&mut cx, |picker, _| {
+                    let delegate = &mut picker.delegate;
+                    delegate.matches = matches;
+                    if delegate.matches.is_empty() {
+                        delegate.selected_index = 0;
+                    } else {
+                        delegate.selected_index =
+                            core::cmp::min(delegate.selected_index, delegate.matches.len() - 1);
+                    }
+                    delegate.last_query = query;
+                })
+                .log_err();
+        })
+    }
+
+    fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
+        let current_pick = self.selected_index();
+        let Some(current_pick) = self
+            .matches
+            .get(current_pick)
+            .map(|pick| pick.string.clone())
+        else {
+            return;
+        };
+        cx.spawn(|picker, mut cx| async move {
+            picker
+                .update(&mut cx, |this, cx| {
+                    let project = this.delegate.workspace.read(cx).project().read(cx);
+                    let mut cwd = project
+                        .visible_worktrees(cx)
+                        .next()
+                        .ok_or_else(|| anyhow!("There are no visisible worktrees."))?
+                        .read(cx)
+                        .abs_path()
+                        .to_path_buf();
+                    cwd.push(".git");
+                    let status = project
+                        .fs()
+                        .open_repo(&cwd)
+                        .ok_or_else(|| {
+                            anyhow!(
+                                "Could not open repository at path `{}`",
+                                cwd.as_os_str().to_string_lossy()
+                            )
+                        })?
+                        .lock()
+                        .change_branch(&current_pick);
+                    if status.is_err() {
+                        this.delegate.display_error_toast(format!("Failed to checkout branch '{current_pick}', check for conflicts or unstashed files"), cx);
+                        status?;
+                    }
+                    cx.emit(DismissEvent);
+
+                    Ok::<(), anyhow::Error>(())
+                })
+                .log_err();
+        })
+        .detach();
+    }
+
+    fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
+        cx.emit(DismissEvent);
+    }
+
+    fn render_match(
+        &self,
+        ix: usize,
+        selected: bool,
+        _cx: &mut ViewContext<Picker<Self>>,
+    ) -> Option<Self::ListItem> {
+        let hit = &self.matches[ix];
+        let shortened_branch_name =
+            util::truncate_and_trailoff(&hit.string, self.branch_name_trailoff_after);
+        let highlights: Vec<_> = hit
+            .positions
+            .iter()
+            .filter(|index| index < &&self.branch_name_trailoff_after)
+            .copied()
+            .collect();
+        Some(
+            ListItem::new(SharedString::from(format!("vcs-menu-{ix}")))
+                .start_slot(HighlightedLabel::new(shortened_branch_name, highlights))
+                .selected(selected),
+        )
+    }
+    // fn render_header(
+    //     &self,
+    //     cx: &mut ViewContext<Picker<Self>>,
+    // ) -> Option<AnyElement<Picker<Self>>> {
+    //     let theme = &theme::current(cx);
+    //     let style = theme.picker.header.clone();
+    //     let label = if self.last_query.is_empty() {
+    //         Flex::row()
+    //             .with_child(Label::new("Recent branches", style.label.clone()))
+    //             .contained()
+    //             .with_style(style.container)
+    //     } else {
+    //         Flex::row()
+    //             .with_child(Label::new("Branches", style.label.clone()))
+    //             .with_children(self.matches.is_empty().not().then(|| {
+    //                 let suffix = if self.matches.len() == 1 { "" } else { "es" };
+    //                 Label::new(
+    //                     format!("{} match{}", self.matches.len(), suffix),
+    //                     style.label,
+    //                 )
+    //                 .flex_float()
+    //             }))
+    //             .contained()
+    //             .with_style(style.container)
+    //     };
+    //     Some(label.into_any())
+    // }
+    // fn render_footer(
+    //     &self,
+    //     cx: &mut ViewContext<Picker<Self>>,
+    // ) -> Option<AnyElement<Picker<Self>>> {
+    //     if !self.last_query.is_empty() {
+    //         let theme = &theme::current(cx);
+    //         let style = theme.picker.footer.clone();
+    //         enum BranchCreateButton {}
+    //         Some(
+    //             Flex::row().with_child(MouseEventHandler::new::<BranchCreateButton, _>(0, cx, |state, _| {
+    //                 let style = style.style_for(state);
+    //                 Label::new("Create branch", style.label.clone())
+    //                     .contained()
+    //                     .with_style(style.container)
+    //             })
+    //             .with_cursor_style(CursorStyle::PointingHand)
+    //             .on_down(MouseButton::Left, |_, _, cx| {
+    //                 cx.spawn(|picker, mut cx| async move {
+    //                     picker.update(&mut cx, |this, cx| {
+    //                         let project = this.delegate().workspace.read(cx).project().read(cx);
+    //                         let current_pick = &this.delegate().last_query;
+    //                         let mut cwd = project
+    //                         .visible_worktrees(cx)
+    //                         .next()
+    //                         .ok_or_else(|| anyhow!("There are no visisible worktrees."))?
+    //                         .read(cx)
+    //                         .abs_path()
+    //                         .to_path_buf();
+    //                         cwd.push(".git");
+    //                         let repo = project
+    //                             .fs()
+    //                             .open_repo(&cwd)
+    //                             .ok_or_else(|| anyhow!("Could not open repository at path `{}`", cwd.as_os_str().to_string_lossy()))?;
+    //                         let repo = repo
+    //                             .lock();
+    //                         let status = repo
+    //                             .create_branch(&current_pick);
+    //                         if status.is_err() {
+    //                             this.delegate().display_error_toast(format!("Failed to create branch '{current_pick}', check for conflicts or unstashed files"), cx);
+    //                             status?;
+    //                         }
+    //                         let status = repo.change_branch(&current_pick);
+    //                         if status.is_err() {
+    //                             this.delegate().display_error_toast(format!("Failed to chec branch '{current_pick}', check for conflicts or unstashed files"), cx);
+    //                             status?;
+    //                         }
+    //                         cx.emit(PickerEvent::Dismiss);
+    //                         Ok::<(), anyhow::Error>(())
+    //             })
+    //                 }).detach();
+    //             })).aligned().right()
+    //             .into_any(),
+    //         )
+    //     } else {
+    //         None
+    //     }
+    // }
+}

crates/workspace2/src/dock.rs πŸ”—

@@ -1,5 +1,5 @@
+use crate::DraggedDock;
 use crate::{status_bar::StatusItemView, Workspace};
-use crate::{DockClickReset, DockDragState};
 use gpui::{
     div, px, Action, AnchorCorner, AnyView, AppContext, Axis, ClickEvent, Div, Entity, EntityId,
     EventEmitter, FocusHandle, FocusableView, IntoElement, MouseButton, ParentElement, Render,
@@ -41,7 +41,7 @@ pub trait Panel: FocusableView + EventEmitter<PanelEvent> {
 }
 
 pub trait PanelHandle: Send + Sync {
-    fn entity_id(&self) -> EntityId;
+    fn panel_id(&self) -> EntityId;
     fn persistent_name(&self) -> &'static str;
     fn position(&self, cx: &WindowContext) -> DockPosition;
     fn position_is_valid(&self, position: DockPosition, cx: &WindowContext) -> bool;
@@ -62,7 +62,7 @@ impl<T> PanelHandle for View<T>
 where
     T: Panel,
 {
-    fn entity_id(&self) -> EntityId {
+    fn panel_id(&self) -> EntityId {
         Entity::entity_id(self)
     }
 
@@ -135,7 +135,7 @@ pub struct Dock {
     is_open: bool,
     active_panel_index: usize,
     focus_handle: FocusHandle,
-    focus_subscription: Subscription,
+    _focus_subscription: Subscription,
 }
 
 impl FocusableView for Dock {
@@ -187,7 +187,6 @@ struct PanelEntry {
 
 pub struct PanelButtons {
     dock: View<Dock>,
-    workspace: WeakView<Workspace>,
 }
 
 impl Dock {
@@ -204,7 +203,7 @@ impl Dock {
             active_panel_index: 0,
             is_open: false,
             focus_handle,
-            focus_subscription,
+            _focus_subscription: focus_subscription,
         }
     }
 
@@ -261,7 +260,7 @@ impl Dock {
 
     pub fn set_panel_zoomed(&mut self, panel: &AnyView, zoomed: bool, cx: &mut ViewContext<Self>) {
         for entry in &mut self.panel_entries {
-            if entry.panel.entity_id() == panel.entity_id() {
+            if entry.panel.panel_id() == panel.entity_id() {
                 if zoomed != entry.panel.is_zoomed(cx) {
                     entry.panel.set_zoomed(zoomed, cx);
                 }
@@ -309,7 +308,7 @@ impl Dock {
 
                     let was_visible = this.is_open()
                         && this.visible_panel().map_or(false, |active_panel| {
-                            active_panel.entity_id() == Entity::entity_id(&panel)
+                            active_panel.panel_id() == Entity::entity_id(&panel)
                         });
 
                     this.remove_panel(&panel, cx);
@@ -351,7 +350,7 @@ impl Dock {
                     if let Some(ix) = this
                         .panel_entries
                         .iter()
-                        .position(|entry| entry.panel.entity_id() == Entity::entity_id(&panel))
+                        .position(|entry| entry.panel.panel_id() == Entity::entity_id(&panel))
                     {
                         this.set_open(true, cx);
                         this.activate_panel(ix, cx);
@@ -361,7 +360,7 @@ impl Dock {
                 PanelEvent::Close => {
                     if this
                         .visible_panel()
-                        .map_or(false, |p| p.entity_id() == Entity::entity_id(&panel))
+                        .map_or(false, |p| p.panel_id() == Entity::entity_id(&panel))
                     {
                         this.set_open(false, cx);
                     }
@@ -389,7 +388,7 @@ impl Dock {
         if let Some(panel_ix) = self
             .panel_entries
             .iter()
-            .position(|entry| entry.panel.entity_id() == Entity::entity_id(panel))
+            .position(|entry| entry.panel.panel_id() == Entity::entity_id(panel))
         {
             if panel_ix == self.active_panel_index {
                 self.active_panel_index = 0;
@@ -450,7 +449,7 @@ impl Dock {
     pub fn panel_size(&self, panel: &dyn PanelHandle, cx: &WindowContext) -> Option<f32> {
         self.panel_entries
             .iter()
-            .find(|entry| entry.panel.entity_id() == panel.entity_id())
+            .find(|entry| entry.panel.panel_id() == panel.panel_id())
             .map(|entry| entry.panel.size(cx))
     }
 
@@ -487,62 +486,72 @@ impl Render for Dock {
         if let Some(entry) = self.visible_entry() {
             let size = entry.panel.size(cx);
 
-            let mut pre_resize_handle = None;
-            let mut post_resize_handle = None;
             let position = self.position;
-            let handler = div()
+            let mut handle = div()
                 .id("resize-handle")
-                .bg(cx.theme().colors().border)
-                .on_mouse_down(gpui::MouseButton::Left, move |_, cx| {
-                    cx.update_global(|drag: &mut DockDragState, cx| drag.0 = Some(position))
+                .on_drag(DraggedDock(position), |dock, cx| {
+                    cx.build_view(|_| dock.clone())
                 })
                 .on_click(cx.listener(|v, e: &ClickEvent, cx| {
-                    if e.down.button == MouseButton::Left {
-                        cx.update_global(|state: &mut DockClickReset, cx| {
-                            if state.0.is_some() {
-                                state.0 = None;
-                                v.resize_active_panel(None, cx)
-                            } else {
-                                let double_click = cx.double_click_interval();
-                                let timer = cx.background_executor().timer(double_click);
-                                state.0 = Some(cx.spawn(|_, mut cx| async move {
-                                    timer.await;
-                                    cx.update_global(|state: &mut DockClickReset, cx| {
-                                        state.0 = None;
-                                    })
-                                    .ok();
-                                }));
-                            }
-                        })
+                    if e.down.button == MouseButton::Left && e.down.click_count == 2 {
+                        v.resize_active_panel(None, cx)
                     }
-                }));
+                }))
+                .z_index(1);
+
+            const HANDLE_SIZE: Pixels = Pixels(6.);
 
             match self.position() {
                 DockPosition::Left => {
-                    post_resize_handle = Some(handler.w_1().h_full().cursor_col_resize())
+                    handle = handle
+                        .absolute()
+                        .right(px(0.))
+                        .top(px(0.))
+                        .h_full()
+                        .w(HANDLE_SIZE)
+                        .cursor_col_resize();
                 }
                 DockPosition::Bottom => {
-                    pre_resize_handle = Some(handler.w_full().h_1().cursor_row_resize())
+                    handle = handle
+                        .absolute()
+                        .top(px(0.))
+                        .left(px(0.))
+                        .w_full()
+                        .h(HANDLE_SIZE)
+                        .cursor_row_resize();
                 }
                 DockPosition::Right => {
-                    pre_resize_handle = Some(handler.w_full().h_1().cursor_col_resize())
+                    handle = handle
+                        .absolute()
+                        .top(px(0.))
+                        .left(px(0.))
+                        .w_full()
+                        .h(HANDLE_SIZE)
+                        .cursor_col_resize();
                 }
             }
 
             div()
+                .flex()
                 .border_color(cx.theme().colors().border)
                 .map(|this| match self.position().axis() {
-                    Axis::Horizontal => this.w(px(size)).h_full(),
-                    Axis::Vertical => this.h(px(size)).w_full(),
+                    Axis::Horizontal => this.w(px(size)).h_full().flex_row(),
+                    Axis::Vertical => this.h(px(size)).w_full().flex_col(),
                 })
                 .map(|this| match self.position() {
                     DockPosition::Left => this.border_r(),
                     DockPosition::Right => this.border_l(),
                     DockPosition::Bottom => this.border_t(),
                 })
-                .children(pre_resize_handle)
-                .child(entry.panel.to_any())
-                .children(post_resize_handle)
+                .child(
+                    div()
+                        .map(|this| match self.position().axis() {
+                            Axis::Horizontal => this.min_w(px(size)).h_full(),
+                            Axis::Vertical => this.min_h(px(size)).w_full(),
+                        })
+                        .child(entry.panel.to_any()),
+                )
+                .child(handle)
         } else {
             div()
         }
@@ -550,166 +559,12 @@ impl Render for Dock {
 }
 
 impl PanelButtons {
-    pub fn new(
-        dock: View<Dock>,
-        workspace: WeakView<Workspace>,
-        cx: &mut ViewContext<Self>,
-    ) -> Self {
+    pub fn new(dock: View<Dock>, cx: &mut ViewContext<Self>) -> Self {
         cx.observe(&dock, |_, _, cx| cx.notify()).detach();
-        Self { dock, workspace }
+        Self { dock }
     }
 }
 
-// impl Render for PanelButtons {
-//     type Element = ();
-
-//     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
-//         todo!("")
-//     }
-
-//     fn ui_name() -> &'static str {
-//         "PanelButtons"
-//     }
-
-//     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-//         let theme = &settings::get::<ThemeSettings>(cx).theme;
-//         let tooltip_style = theme.tooltip.clone();
-//         let theme = &theme.workspace.status_bar.panel_buttons;
-//         let button_style = theme.button.clone();
-//         let dock = self.dock.read(cx);
-//         let active_ix = dock.active_panel_index;
-//         let is_open = dock.is_open;
-//         let dock_position = dock.position;
-//         let group_style = match dock_position {
-//             DockPosition::Left => theme.group_left,
-//             DockPosition::Bottom => theme.group_bottom,
-//             DockPosition::Right => theme.group_right,
-//         };
-//         let menu_corner = match dock_position {
-//             DockPosition::Left => AnchorCorner::BottomLeft,
-//             DockPosition::Bottom | DockPosition::Right => AnchorCorner::BottomRight,
-//         };
-
-//         let panels = dock
-//             .panel_entries
-//             .iter()
-//             .map(|item| (item.panel.clone(), item.context_menu.clone()))
-//             .collect::<Vec<_>>();
-//         Flex::row()
-//             .with_children(panels.into_iter().enumerate().filter_map(
-//                 |(panel_ix, (view, context_menu))| {
-//                     let icon_path = view.icon_path(cx)?;
-//                     let is_active = is_open && panel_ix == active_ix;
-//                     let (tooltip, tooltip_action) = if is_active {
-//                         (
-//                             format!("Close {} dock", dock_position.to_label()),
-//                             Some(match dock_position {
-//                                 DockPosition::Left => crate::ToggleLeftDock.boxed_clone(),
-//                                 DockPosition::Bottom => crate::ToggleBottomDock.boxed_clone(),
-//                                 DockPosition::Right => crate::ToggleRightDock.boxed_clone(),
-//                             }),
-//                         )
-//                     } else {
-//                         view.icon_tooltip(cx)
-//                     };
-//                     Some(
-//                         Stack::new()
-//                             .with_child(
-//                                 MouseEventHandler::new::<Self, _>(panel_ix, cx, |state, cx| {
-//                                     let style = button_style.in_state(is_active);
-
-//                                     let style = style.style_for(state);
-//                                     Flex::row()
-//                                         .with_child(
-//                                             Svg::new(icon_path)
-//                                                 .with_color(style.icon_color)
-//                                                 .constrained()
-//                                                 .with_width(style.icon_size)
-//                                                 .aligned(),
-//                                         )
-//                                         .with_children(if let Some(label) = view.icon_label(cx) {
-//                                             Some(
-//                                                 Label::new(label, style.label.text.clone())
-//                                                     .contained()
-//                                                     .with_style(style.label.container)
-//                                                     .aligned(),
-//                                             )
-//                                         } else {
-//                                             None
-//                                         })
-//                                         .constrained()
-//                                         .with_height(style.icon_size)
-//                                         .contained()
-//                                         .with_style(style.container)
-//                                 })
-//                                 .with_cursor_style(CursorStyle::PointingHand)
-//                                 .on_click(MouseButton::Left, {
-//                                     let tooltip_action =
-//                                         tooltip_action.as_ref().map(|action| action.boxed_clone());
-//                                     move |_, this, cx| {
-//                                         if let Some(tooltip_action) = &tooltip_action {
-//                                             let window = cx.window();
-//                                             let view_id = this.workspace.id();
-//                                             let tooltip_action = tooltip_action.boxed_clone();
-//                                             cx.spawn(|_, mut cx| async move {
-//                                                 window.dispatch_action(
-//                                                     view_id,
-//                                                     &*tooltip_action,
-//                                                     &mut cx,
-//                                                 );
-//                                             })
-//                                             .detach();
-//                                         }
-//                                     }
-//                                 })
-//                                 .on_click(MouseButton::Right, {
-//                                     let view = view.clone();
-//                                     let menu = context_menu.clone();
-//                                     move |_, _, cx| {
-//                                         const POSITIONS: [DockPosition; 3] = [
-//                                             DockPosition::Left,
-//                                             DockPosition::Right,
-//                                             DockPosition::Bottom,
-//                                         ];
-
-//                                         menu.update(cx, |menu, cx| {
-//                                             let items = POSITIONS
-//                                                 .into_iter()
-//                                                 .filter(|position| {
-//                                                     *position != dock_position
-//                                                         && view.position_is_valid(*position, cx)
-//                                                 })
-//                                                 .map(|position| {
-//                                                     let view = view.clone();
-//                                                     ContextMenuItem::handler(
-//                                                         format!("Dock {}", position.to_label()),
-//                                                         move |cx| view.set_position(position, cx),
-//                                                     )
-//                                                 })
-//                                                 .collect();
-//                                             menu.show(Default::default(), menu_corner, items, cx);
-//                                         })
-//                                     }
-//                                 })
-//                                 .with_tooltip::<Self>(
-//                                     panel_ix,
-//                                     tooltip,
-//                                     tooltip_action,
-//                                     tooltip_style.clone(),
-//                                     cx,
-//                                 ),
-//                             )
-//                             .with_child(ChildView::new(&context_menu, cx)),
-//                     )
-//                 },
-//             ))
-//             .contained()
-//             .with_style(group_style)
-//             .into_any()
-//     }
-// }
-
-// here be kittens
 impl Render for PanelButtons {
     type Element = Div;
 

crates/workspace2/src/item.rs πŸ”—

@@ -919,7 +919,7 @@ pub mod test {
     impl EventEmitter<ItemEvent> for TestItem {}
 
     impl FocusableView for TestItem {
-        fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
+        fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
             self.focus_handle.clone()
         }
     }
@@ -941,8 +941,8 @@ pub mod test {
         fn tab_content(
             &self,
             detail: Option<usize>,
-            selected: bool,
-            cx: &ui::prelude::WindowContext,
+            _selected: bool,
+            _cx: &ui::prelude::WindowContext,
         ) -> AnyElement {
             self.tab_detail.set(detail);
             gpui::div().into_any_element()

crates/workspace2/src/modal_layer.rs πŸ”—

@@ -5,7 +5,7 @@ use gpui::{
 use ui::{h_stack, v_stack};
 
 pub trait ModalView: ManagedView {
-    fn on_before_dismiss(&mut self, cx: &mut ViewContext<Self>) -> bool {
+    fn on_before_dismiss(&mut self, _: &mut ViewContext<Self>) -> bool {
         true
     }
 }
@@ -27,7 +27,7 @@ impl<V: ModalView> ModalViewHandle for View<V> {
 
 pub struct ActiveModal {
     modal: Box<dyn ModalViewHandle>,
-    subscription: Subscription,
+    _subscription: Subscription,
     previous_focus_handle: Option<FocusHandle>,
     focus_handle: FocusHandle,
 }
@@ -63,7 +63,7 @@ impl ModalLayer {
     {
         self.active_modal = Some(ActiveModal {
             modal: Box::new(new_modal.clone()),
-            subscription: cx.subscribe(&new_modal, |this, modal, _: &DismissEvent, cx| {
+            _subscription: cx.subscribe(&new_modal, |this, _, _: &DismissEvent, cx| {
                 this.hide_modal(cx);
             }),
             previous_focus_handle: cx.focused(),
@@ -116,7 +116,7 @@ impl Render for ModalLayer {
             .size_full()
             .top_0()
             .left_0()
-            .z_index(400)
+            .z_index(169)
             .child(
                 v_stack()
                     .h(px(0.0))

crates/workspace2/src/notifications.rs πŸ”—

@@ -104,12 +104,9 @@ impl Workspace {
             })
         {
             let notification = build_notification(cx);
-            cx.subscribe(
-                &notification,
-                move |this, handle, event: &DismissEvent, cx| {
-                    this.dismiss_notification_internal(type_id, id, cx);
-                },
-            )
+            cx.subscribe(&notification, move |this, _, _: &DismissEvent, cx| {
+                this.dismiss_notification_internal(type_id, id, cx);
+            })
             .detach();
             self.notifications
                 .push((type_id, id, Box::new(notification)));
@@ -173,21 +170,15 @@ impl Workspace {
 
 pub mod simple_message_notification {
     use gpui::{
-        div, AnyElement, AppContext, DismissEvent, Div, EventEmitter, InteractiveElement,
-        ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, TextStyle,
-        ViewContext,
+        div, DismissEvent, Div, EventEmitter, InteractiveElement, ParentElement, Render,
+        SharedString, StatefulInteractiveElement, Styled, ViewContext,
     };
     use std::sync::Arc;
     use ui::prelude::*;
     use ui::{h_stack, v_stack, Button, Icon, IconElement, Label, StyledExt};
 
-    enum NotificationMessage {
-        Text(SharedString),
-        Element(fn(TextStyle, &AppContext) -> AnyElement),
-    }
-
     pub struct MessageNotification {
-        message: NotificationMessage,
+        message: SharedString,
         on_click: Option<Arc<dyn Fn(&mut ViewContext<Self>)>>,
         click_message: Option<SharedString>,
     }
@@ -200,23 +191,12 @@ pub mod simple_message_notification {
             S: Into<SharedString>,
         {
             Self {
-                message: NotificationMessage::Text(message.into()),
+                message: message.into(),
                 on_click: None,
                 click_message: None,
             }
         }
 
-        // not needed I think (only for the "new panel" toast, which is outdated now)
-        // pub fn new_element(
-        //     message: fn(TextStyle, &AppContext) -> AnyElement,
-        // ) -> MessageNotification {
-        //     Self {
-        //         message: NotificationMessage::Element(message),
-        //         on_click: None,
-        //         click_message: None,
-        //     }
-        // }
-
         pub fn with_click_message<S>(mut self, message: S) -> Self
         where
             S: Into<SharedString>,
@@ -248,18 +228,13 @@ pub mod simple_message_notification {
                 .child(
                     h_stack()
                         .justify_between()
-                        .child(div().max_w_80().child(match &self.message {
-                            NotificationMessage::Text(text) => Label::new(text.clone()),
-                            NotificationMessage::Element(element) => {
-                                todo!()
-                            }
-                        }))
+                        .child(div().max_w_80().child(Label::new(self.message.clone())))
                         .child(
                             div()
                                 .id("cancel")
                                 .child(IconElement::new(Icon::Close))
                                 .cursor_pointer()
-                                .on_click(cx.listener(|this, event, cx| this.dismiss(cx))),
+                                .on_click(cx.listener(|this, _, cx| this.dismiss(cx))),
                         ),
                 )
                 .children(self.click_message.iter().map(|message| {

crates/workspace2/src/pane.rs πŸ”—

@@ -7,10 +7,11 @@ use crate::{
 use anyhow::Result;
 use collections::{HashMap, HashSet, VecDeque};
 use gpui::{
-    actions, impl_actions, overlay, prelude::*, Action, AnchorCorner, AnyWeakView, AppContext,
-    AsyncWindowContext, DismissEvent, Div, EntityId, EventEmitter, FocusHandle, Focusable,
-    FocusableView, Model, MouseButton, NavigationDirection, Pixels, Point, PromptLevel, Render,
-    ScrollHandle, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowContext,
+    actions, impl_actions, overlay, prelude::*, Action, AnchorCorner, AppContext,
+    AsyncWindowContext, DismissEvent, Div, DragMoveEvent, EntityId, EventEmitter, FocusHandle,
+    Focusable, FocusableView, Model, MouseButton, NavigationDirection, Pixels, Point, PromptLevel,
+    Render, ScrollHandle, Subscription, Task, View, ViewContext, VisualContext, WeakView,
+    WindowContext,
 };
 use parking_lot::Mutex;
 use project::{Project, ProjectEntryId, ProjectPath};
@@ -28,8 +29,8 @@ use std::{
 use theme::ThemeSettings;
 
 use ui::{
-    h_stack, prelude::*, right_click_menu, ButtonSize, Color, Icon, IconButton, IconSize,
-    Indicator, Label, Tab, TabBar, TabPosition, Tooltip,
+    prelude::*, right_click_menu, ButtonSize, Color, Icon, IconButton, IconSize, Indicator, Label,
+    Tab, TabBar, TabPosition, Tooltip,
 };
 use ui::{v_stack, ContextMenu};
 use util::{maybe, truncate_and_remove_front, ResultExt};
@@ -164,11 +165,6 @@ impl fmt::Debug for Event {
     }
 }
 
-struct FocusedView {
-    view: AnyWeakView,
-    focus_handle: FocusHandle,
-}
-
 pub struct Pane {
     focus_handle: FocusHandle,
     items: Vec<Box<dyn ItemHandle>>,
@@ -184,10 +180,11 @@ pub struct Pane {
     //     tab_context_menu: ViewHandle<ContextMenu>,
     workspace: WeakView<Workspace>,
     project: Model<Project>,
+    drag_split_direction: Option<SplitDirection>,
     //     can_drop: Rc<dyn Fn(&DragAndDrop<Workspace>, &WindowContext) -> bool>,
     can_split: bool,
     //     render_tab_bar_buttons: Rc<dyn Fn(&mut Pane, &mut ViewContext<Pane>) -> AnyElement<Pane>>,
-    subscriptions: Vec<Subscription>,
+    _subscriptions: Vec<Subscription>,
     tab_bar_scroll_handle: ScrollHandle,
 }
 
@@ -231,6 +228,7 @@ pub struct NavigationEntry {
     pub timestamp: usize,
 }
 
+#[derive(Clone)]
 struct DraggedTab {
     pub pane: View<Pane>,
     pub ix: usize,
@@ -365,6 +363,7 @@ impl Pane {
             new_item_menu: None,
             split_item_menu: None,
             tab_bar_scroll_handle: ScrollHandle::new(),
+            drag_split_direction: None,
             // tab_bar_context_menu: TabBarContextMenu {
             //     kind: TabBarContextMenuKind::New,
             //     handle: context_menu,
@@ -431,14 +430,10 @@ impl Pane {
             //         })
             //         .into_any()
             // }),
-            subscriptions,
+            _subscriptions: subscriptions,
         }
     }
 
-    pub(crate) fn workspace(&self) -> &WeakView<Workspace> {
-        &self.workspace
-    }
-
     pub fn has_focus(&self, cx: &WindowContext) -> bool {
         // todo!(); // inline this manually
         self.focus_handle.contains_focused(cx)
@@ -1467,21 +1462,6 @@ impl Pane {
         let label = item.tab_content(Some(detail), is_active, cx);
         let close_side = &ItemSettings::get_global(cx).close_position;
 
-        let (text_color, tab_bg, tab_hover_bg, tab_active_bg) = match ix == self.active_item_index {
-            false => (
-                cx.theme().colors().text_muted,
-                cx.theme().colors().tab_inactive_background,
-                cx.theme().colors().ghost_element_hover,
-                cx.theme().colors().ghost_element_active,
-            ),
-            true => (
-                cx.theme().colors().text,
-                cx.theme().colors().tab_active_background,
-                cx.theme().colors().element_hover,
-                cx.theme().colors().element_active,
-            ),
-        };
-
         let indicator = maybe!({
             let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
                 (true, _) => Color::Warning,
@@ -1497,56 +1477,57 @@ impl Pane {
         let is_last_item = ix == self.items.len() - 1;
         let position_relative_to_active_item = ix.cmp(&self.active_item_index);
 
-        let tab =
-            Tab::new(ix)
-                .position(if is_first_item {
-                    TabPosition::First
-                } else if is_last_item {
-                    TabPosition::Last
-                } else {
-                    TabPosition::Middle(position_relative_to_active_item)
-                })
-                .close_side(match close_side {
-                    ClosePosition::Left => ui::TabCloseSide::Start,
-                    ClosePosition::Right => ui::TabCloseSide::End,
-                })
-                .selected(is_active)
-                .on_click(cx.listener(move |pane: &mut Self, event, cx| {
-                    pane.activate_item(ix, true, true, cx)
-                }))
-                .on_drag({
-                    let pane = cx.view().clone();
-                    move |cx| {
-                        cx.build_view(|cx| DraggedTab {
-                            pane: pane.clone(),
-                            detail,
-                            item_id,
-                            is_active,
-                            ix,
-                        })
-                    }
-                })
-                .drag_over::<DraggedTab>(|tab| tab.bg(cx.theme().colors().tab_active_background))
-                .on_drop(
-                    cx.listener(move |this, dragged_tab: &View<DraggedTab>, cx| {
-                        this.handle_tab_drop(dragged_tab, ix, cx)
-                    }),
-                )
-                .when_some(item.tab_tooltip_text(cx), |tab, text| {
-                    tab.tooltip(move |cx| Tooltip::text(text.clone(), cx))
-                })
-                .start_slot::<Indicator>(indicator)
-                .end_slot(
-                    IconButton::new("close tab", Icon::Close)
-                        .icon_color(Color::Muted)
-                        .size(ButtonSize::None)
-                        .icon_size(IconSize::XSmall)
-                        .on_click(cx.listener(move |pane, _, cx| {
-                            pane.close_item_by_id(item_id, SaveIntent::Close, cx)
-                                .detach_and_log_err(cx);
-                        })),
-                )
-                .child(label);
+        let tab = Tab::new(ix)
+            .position(if is_first_item {
+                TabPosition::First
+            } else if is_last_item {
+                TabPosition::Last
+            } else {
+                TabPosition::Middle(position_relative_to_active_item)
+            })
+            .close_side(match close_side {
+                ClosePosition::Left => ui::TabCloseSide::Start,
+                ClosePosition::Right => ui::TabCloseSide::End,
+            })
+            .selected(is_active)
+            .on_click(
+                cx.listener(move |pane: &mut Self, _, cx| pane.activate_item(ix, true, true, cx)),
+            )
+            .on_drag(
+                DraggedTab {
+                    pane: cx.view().clone(),
+                    detail,
+                    item_id,
+                    is_active,
+                    ix,
+                },
+                |tab, cx| cx.build_view(|_| tab.clone()),
+            )
+            .drag_over::<DraggedTab>(|tab| tab.bg(cx.theme().colors().tab_active_background))
+            .drag_over::<ProjectEntryId>(|tab| tab.bg(gpui::red()))
+            .on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
+                this.drag_split_direction = None;
+                this.handle_tab_drop(dragged_tab, ix, cx)
+            }))
+            .on_drop(cx.listener(move |this, entry_id: &ProjectEntryId, cx| {
+                this.drag_split_direction = None;
+                this.handle_project_entry_drop(entry_id, cx)
+            }))
+            .when_some(item.tab_tooltip_text(cx), |tab, text| {
+                tab.tooltip(move |cx| Tooltip::text(text.clone(), cx))
+            })
+            .start_slot::<Indicator>(indicator)
+            .end_slot(
+                IconButton::new("close tab", Icon::Close)
+                    .icon_color(Color::Muted)
+                    .size(ButtonSize::None)
+                    .icon_size(IconSize::XSmall)
+                    .on_click(cx.listener(move |pane, _, cx| {
+                        pane.close_item_by_id(item_id, SaveIntent::Close, cx)
+                            .detach_and_log_err(cx);
+                    })),
+            )
+            .child(label);
 
         let single_entry_to_resolve = {
             let item_entries = self.items[ix].project_entry_ids(cx);
@@ -1616,12 +1597,12 @@ impl Pane {
                         IconButton::new("plus", Icon::Plus)
                             .icon_size(IconSize::Small)
                             .on_click(cx.listener(|this, _, cx| {
-                                let menu = ContextMenu::build(cx, |menu, cx| {
+                                let menu = ContextMenu::build(cx, |menu, _| {
                                     menu.action("New File", NewFile.boxed_clone())
                                         .action("New Terminal", NewCenterTerminal.boxed_clone())
                                         .action("New Search", NewSearch.boxed_clone())
                                 });
-                                cx.subscribe(&menu, |this, _, event: &DismissEvent, cx| {
+                                cx.subscribe(&menu, |this, _, _: &DismissEvent, cx| {
                                     this.focus(cx);
                                     this.new_item_menu = None;
                                 })
@@ -1640,13 +1621,13 @@ impl Pane {
                         IconButton::new("split", Icon::Split)
                             .icon_size(IconSize::Small)
                             .on_click(cx.listener(|this, _, cx| {
-                                let menu = ContextMenu::build(cx, |menu, cx| {
+                                let menu = ContextMenu::build(cx, |menu, _| {
                                     menu.action("Split Right", SplitRight.boxed_clone())
                                         .action("Split Left", SplitLeft.boxed_clone())
                                         .action("Split Up", SplitUp.boxed_clone())
                                         .action("Split Down", SplitDown.boxed_clone())
                                 });
-                                cx.subscribe(&menu, |this, _, event: &DismissEvent, cx| {
+                                cx.subscribe(&menu, |this, _, _: &DismissEvent, cx| {
                                     this.focus(cx);
                                     this.split_item_menu = None;
                                 })
@@ -1677,11 +1658,15 @@ impl Pane {
                     .drag_over::<DraggedTab>(|bar| {
                         bar.bg(cx.theme().colors().tab_active_background)
                     })
-                    .on_drop(
-                        cx.listener(move |this, dragged_tab: &View<DraggedTab>, cx| {
-                            this.handle_tab_drop(dragged_tab, this.items.len(), cx)
-                        }),
-                    ),
+                    .drag_over::<ProjectEntryId>(|bar| bar.bg(gpui::red()))
+                    .on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
+                        this.drag_split_direction = None;
+                        this.handle_tab_drop(dragged_tab, this.items.len(), cx)
+                    }))
+                    .on_drop(cx.listener(move |this, entry_id: &ProjectEntryId, cx| {
+                        this.drag_split_direction = None;
+                        this.handle_project_entry_drop(entry_id, cx)
+                    })),
             )
     }
 
@@ -1741,24 +1726,75 @@ impl Pane {
         self.zoomed
     }
 
+    fn handle_drag_move<T>(&mut self, event: &DragMoveEvent<T>, cx: &mut ViewContext<Self>) {
+        let edge_width = cx.rem_size() * 8;
+        let cursor = event.event.position;
+        let direction = if cursor.x < event.bounds.left() + edge_width {
+            Some(SplitDirection::Left)
+        } else if cursor.x > event.bounds.right() - edge_width {
+            Some(SplitDirection::Right)
+        } else if cursor.y < event.bounds.top() + edge_width {
+            Some(SplitDirection::Up)
+        } else if cursor.y > event.bounds.bottom() - edge_width {
+            Some(SplitDirection::Down)
+        } else {
+            None
+        };
+        if direction != self.drag_split_direction {
+            self.drag_split_direction = direction;
+            cx.notify();
+        }
+    }
+
     fn handle_tab_drop(
         &mut self,
-        dragged_tab: &View<DraggedTab>,
+        dragged_tab: &DraggedTab,
         ix: usize,
         cx: &mut ViewContext<'_, Pane>,
     ) {
-        let dragged_tab = dragged_tab.read(cx);
+        let mut to_pane = cx.view().clone();
+        let split_direction = self.drag_split_direction;
         let item_id = dragged_tab.item_id;
         let from_pane = dragged_tab.pane.clone();
-        let to_pane = cx.view().clone();
         self.workspace
-            .update(cx, |workspace, cx| {
+            .update(cx, |_, cx| {
                 cx.defer(move |workspace, cx| {
+                    if let Some(split_direction) = split_direction {
+                        to_pane = workspace.split_pane(to_pane, split_direction, cx);
+                    }
                     workspace.move_item(from_pane, to_pane, item_id, ix, cx);
                 });
             })
             .log_err();
     }
+
+    fn handle_project_entry_drop(
+        &mut self,
+        project_entry_id: &ProjectEntryId,
+        cx: &mut ViewContext<'_, Pane>,
+    ) {
+        let mut to_pane = cx.view().clone();
+        let split_direction = self.drag_split_direction;
+        let project_entry_id = *project_entry_id;
+        self.workspace
+            .update(cx, |_, cx| {
+                cx.defer(move |workspace, cx| {
+                    if let Some(path) = workspace
+                        .project()
+                        .read(cx)
+                        .path_for_entry(project_entry_id, cx)
+                    {
+                        if let Some(split_direction) = split_direction {
+                            to_pane = workspace.split_pane(to_pane, split_direction, cx);
+                        }
+                        workspace
+                            .open_path(path, Some(to_pane.downgrade()), true, cx)
+                            .detach_and_log_err(cx);
+                    }
+                });
+            })
+            .log_err();
+    }
 }
 
 impl FocusableView for Pane {
@@ -1771,7 +1807,8 @@ impl Render for Pane {
     type Element = Focusable<Div>;
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
-        let this = cx.view().downgrade();
+        let mut drag_target_color = cx.theme().colors().text;
+        drag_target_color.a = 0.5;
 
         v_stack()
             .key_context("Pane")
@@ -1850,16 +1887,60 @@ impl Render for Pane {
                 }),
             )
             .child(self.render_tab_bar(cx))
-            .child(self.toolbar.clone())
-            .child(if let Some(item) = self.active_item() {
-                div().flex().flex_1().child(item.to_any())
-            } else {
-                h_stack()
-                    .items_center()
-                    .size_full()
-                    .justify_center()
-                    .child(Label::new("Open a file or project to get started.").color(Color::Muted))
-            })
+            .child(
+                // main content
+                div()
+                    .flex_1()
+                    .relative()
+                    .group("")
+                    .on_drag_move::<DraggedTab>(cx.listener(Self::handle_drag_move))
+                    .on_drag_move::<ProjectEntryId>(cx.listener(Self::handle_drag_move))
+                    .map(|div| {
+                        if let Some(item) = self.active_item() {
+                            div.flex_col()
+                                .child(self.toolbar.clone())
+                                .child(item.to_any())
+                        } else {
+                            div.flex()
+                                .flex_row()
+                                .items_center()
+                                .size_full()
+                                .justify_center()
+                                .child(
+                                    Label::new("Open a file or project to get started.")
+                                        .color(Color::Muted),
+                                )
+                        }
+                    })
+                    .child(
+                        // drag target
+                        div()
+                            .invisible()
+                            .absolute()
+                            .bg(drag_target_color)
+                            .group_drag_over::<DraggedTab>("", |style| style.visible())
+                            .group_drag_over::<ProjectEntryId>("", |style| style.visible())
+                            .on_drop(cx.listener(move |this, dragged_tab, cx| {
+                                this.handle_tab_drop(dragged_tab, this.active_item_index(), cx)
+                            }))
+                            .on_drop(cx.listener(move |this, entry_id, cx| {
+                                this.handle_project_entry_drop(entry_id, cx)
+                            }))
+                            .map(|div| match self.drag_split_direction {
+                                None => div.top_0().left_0().right_0().bottom_0(),
+                                Some(SplitDirection::Up) => div.top_0().left_0().right_0().h_32(),
+                                Some(SplitDirection::Down) => {
+                                    div.left_0().bottom_0().right_0().h_32()
+                                }
+                                Some(SplitDirection::Left) => {
+                                    div.top_0().left_0().bottom_0().w_32()
+                                }
+                                Some(SplitDirection::Right) => {
+                                    div.top_0().bottom_0().right_0().w_32()
+                                }
+                            }),
+                    ),
+            )
             .on_mouse_down(
                 MouseButton::Navigate(NavigationDirection::Back),
                 cx.listener(|pane, _, cx| {

crates/workspace2/src/pane_group.rs πŸ”—

@@ -16,7 +16,7 @@ const HANDLE_HITBOX_SIZE: f32 = 10.0; //todo!(change this back to 4)
 const HORIZONTAL_MIN_SIZE: f32 = 80.;
 const VERTICAL_MIN_SIZE: f32 = 100.;
 
-#[derive(Clone, PartialEq)]
+#[derive(Clone)]
 pub struct PaneGroup {
     pub(crate) root: Member,
 }
@@ -121,7 +121,7 @@ impl PaneGroup {
     }
 }
 
-#[derive(Clone, PartialEq)]
+#[derive(Clone)]
 pub(crate) enum Member {
     Axis(PaneAxis),
     Pane(View<Pane>),
@@ -426,12 +426,6 @@ pub(crate) struct PaneAxis {
     pub bounding_boxes: Arc<Mutex<Vec<Option<Bounds<Pixels>>>>>,
 }
 
-impl PartialEq for PaneAxis {
-    fn eq(&self, other: &Self) -> bool {
-        todo!()
-    }
-}
-
 impl PaneAxis {
     pub fn new(axis: Axis, members: Vec<Member>) -> Self {
         let flexes = Arc::new(Mutex::new(vec![1.; members.len()]));
@@ -816,7 +810,7 @@ mod element {
                 proposed_current_pixel_change -= current_pixel_change;
             }
 
-            // todo!(reserialize workspace)
+            // todo!(schedule serialize)
             // workspace.schedule_serialize(cx);
             cx.notify();
         }
@@ -851,7 +845,7 @@ mod element {
 
                 cx.on_mouse_event({
                     let dragged_handle = dragged_handle.clone();
-                    move |e: &MouseDownEvent, phase, cx| {
+                    move |e: &MouseDownEvent, phase, _cx| {
                         if phase.bubble() && handle_bounds.contains(&e.position) {
                             dragged_handle.replace(Some(ix));
                         }
@@ -859,7 +853,7 @@ mod element {
                 });
                 cx.on_mouse_event(move |e: &MouseMoveEvent, phase, cx| {
                     let dragged_handle = dragged_handle.borrow();
-                    if *dragged_handle == Some(ix) {
+                    if phase.bubble() && *dragged_handle == Some(ix) {
                         Self::compute_resize(&flexes, e, ix, axis, axis_bounds, cx)
                     }
                 });
@@ -896,7 +890,7 @@ mod element {
         }
 
         fn paint(
-            self,
+            &mut self,
             bounds: gpui::Bounds<ui::prelude::Pixels>,
             state: &mut Self::State,
             cx: &mut ui::prelude::WindowContext,
@@ -912,7 +906,7 @@ mod element {
             let mut bounding_boxes = self.bounding_boxes.lock();
             bounding_boxes.clear();
 
-            for (ix, child) in self.children.into_iter().enumerate() {
+            for (ix, child) in self.children.iter_mut().enumerate() {
                 //todo!(active_pane_magnification)
                 // If usign active pane magnification, need to switch to using
                 // 1 for all non-active panes, and then the magnification for the
@@ -949,7 +943,7 @@ mod element {
             cx.with_z_index(1, |cx| {
                 cx.on_mouse_event({
                     let state = state.clone();
-                    move |e: &MouseUpEvent, phase, cx| {
+                    move |_: &MouseUpEvent, phase, _cx| {
                         if phase.bubble() {
                             state.replace(None);
                         }
@@ -968,402 +962,4 @@ mod element {
     fn flex_values_in_bounds(flexes: &[f32]) -> bool {
         (flexes.iter().copied().sum::<f32>() - flexes.len() as f32).abs() < 0.001
     }
-    //     // use std::{cell::RefCell, iter::from_fn, ops::Range, rc::Rc};
-
-    //     // use gpui::{
-    //     //     geometry::{
-    //     //         rect::Bounds<Pixels>,
-    //     //         vector::{vec2f, Vector2F},
-    //     //     },
-    //     //     json::{self, ToJson},
-    //     //     platform::{CursorStyle, MouseButton},
-    //     //     scene::MouseDrag,
-    //     //     AnyElement, Axis, CursorRegion, Element, EventContext, MouseRegion, Bounds<Pixels>Ext,
-    //     //     SizeConstraint, Vector2FExt, ViewContext,
-    //     // };
-
-    //     use crate::{
-    //         pane_group::{HANDLE_HITBOX_SIZE, HORIZONTAL_MIN_SIZE, VERTICAL_MIN_SIZE},
-    //         Workspace, WorkspaceSettings,
-    //     };
-
-    //     pub struct PaneAxisElement {
-    //         axis: Axis,
-    //         basis: usize,
-    //         active_pane_ix: Option<usize>,
-    //         flexes: Rc<RefCell<Vec<f32>>>,
-    //         children: Vec<AnyElement<Workspace>>,
-    //         bounding_boxes: Rc<RefCell<Vec<Option<Bounds<Pixels>>>>>,
-    //     }
-
-    //     impl PaneAxisElement {
-    //         pub fn new(
-    //             axis: Axis,
-    //             basis: usize,
-    //             flexes: Rc<RefCell<Vec<f32>>>,
-    //             bounding_boxes: Rc<RefCell<Vec<Option<Bounds<Pixels>>>>>,
-    //         ) -> Self {
-    //             Self {
-    //                 axis,
-    //                 basis,
-    //                 flexes,
-    //                 bounding_boxes,
-    //                 active_pane_ix: None,
-    //                 children: Default::default(),
-    //             }
-    //         }
-
-    //         pub fn set_active_pane(&mut self, active_pane_ix: Option<usize>) {
-    //             self.active_pane_ix = active_pane_ix;
-    //         }
-
-    //         fn layout_children(
-    //             &mut self,
-    //             active_pane_magnification: f32,
-    //             constraint: SizeConstraint,
-    //             remaining_space: &mut f32,
-    //             remaining_flex: &mut f32,
-    //             cross_axis_max: &mut f32,
-    //             view: &mut Workspace,
-    //             cx: &mut ViewContext<Workspace>,
-    //         ) {
-    //             let flexes = self.flexes.borrow();
-    //             let cross_axis = self.axis.invert();
-    //             for (ix, child) in self.children.iter_mut().enumerate() {
-    //                 let flex = if active_pane_magnification != 1. {
-    //                     if let Some(active_pane_ix) = self.active_pane_ix {
-    //                         if ix == active_pane_ix {
-    //                             active_pane_magnification
-    //                         } else {
-    //                             1.
-    //                         }
-    //                     } else {
-    //                         1.
-    //                     }
-    //                 } else {
-    //                     flexes[ix]
-    //                 };
-
-    //                 let child_size = if *remaining_flex == 0.0 {
-    //                     *remaining_space
-    //                 } else {
-    //                     let space_per_flex = *remaining_space / *remaining_flex;
-    //                     space_per_flex * flex
-    //                 };
-
-    //                 let child_constraint = match self.axis {
-    //                     Axis::Horizontal => SizeConstraint::new(
-    //                         vec2f(child_size, constraint.min.y()),
-    //                         vec2f(child_size, constraint.max.y()),
-    //                     ),
-    //                     Axis::Vertical => SizeConstraint::new(
-    //                         vec2f(constraint.min.x(), child_size),
-    //                         vec2f(constraint.max.x(), child_size),
-    //                     ),
-    //                 };
-    //                 let child_size = child.layout(child_constraint, view, cx);
-    //                 *remaining_space -= child_size.along(self.axis);
-    //                 *remaining_flex -= flex;
-    //                 *cross_axis_max = cross_axis_max.max(child_size.along(cross_axis));
-    //             }
-    //         }
-
-    //         fn handle_resize(
-    //             flexes: Rc<RefCell<Vec<f32>>>,
-    //             axis: Axis,
-    //             preceding_ix: usize,
-    //             child_start: Vector2F,
-    //             drag_bounds: Bounds<Pixels>,
-    //         ) -> impl Fn(MouseDrag, &mut Workspace, &mut EventContext<Workspace>) {
-    //             let size = move |ix, flexes: &[f32]| {
-    //                 drag_bounds.length_along(axis) * (flexes[ix] / flexes.len() as f32)
-    //             };
-
-    //             move |drag, workspace: &mut Workspace, cx| {
-    //                 if drag.end {
-    //                     // TODO: Clear cascading resize state
-    //                     return;
-    //                 }
-    //                 let min_size = match axis {
-    //                     Axis::Horizontal => HORIZONTAL_MIN_SIZE,
-    //                     Axis::Vertical => VERTICAL_MIN_SIZE,
-    //                 };
-    //                 let mut flexes = flexes.borrow_mut();
-
-    //                 // Don't allow resizing to less than the minimum size, if elements are already too small
-    //                 if min_size - 1. > size(preceding_ix, flexes.as_slice()) {
-    //                     return;
-    //                 }
-
-    //                 let mut proposed_current_pixel_change = (drag.position - child_start).along(axis)
-    //                     - size(preceding_ix, flexes.as_slice());
-
-    //                 let flex_changes = |pixel_dx, target_ix, next: isize, flexes: &[f32]| {
-    //                     let flex_change = pixel_dx / drag_bounds.length_along(axis);
-    //                     let current_target_flex = flexes[target_ix] + flex_change;
-    //                     let next_target_flex =
-    //                         flexes[(target_ix as isize + next) as usize] - flex_change;
-    //                     (current_target_flex, next_target_flex)
-    //                 };
-
-    //                 let mut successors = from_fn({
-    //                     let forward = proposed_current_pixel_change > 0.;
-    //                     let mut ix_offset = 0;
-    //                     let len = flexes.len();
-    //                     move || {
-    //                         let result = if forward {
-    //                             (preceding_ix + 1 + ix_offset < len).then(|| preceding_ix + ix_offset)
-    //                         } else {
-    //                             (preceding_ix as isize - ix_offset as isize >= 0)
-    //                                 .then(|| preceding_ix - ix_offset)
-    //                         };
-
-    //                         ix_offset += 1;
-
-    //                         result
-    //                     }
-    //                 });
-
-    //                 while proposed_current_pixel_change.abs() > 0. {
-    //                     let Some(current_ix) = successors.next() else {
-    //                         break;
-    //                     };
-
-    //                     let next_target_size = f32::max(
-    //                         size(current_ix + 1, flexes.as_slice()) - proposed_current_pixel_change,
-    //                         min_size,
-    //                     );
-
-    //                     let current_target_size = f32::max(
-    //                         size(current_ix, flexes.as_slice())
-    //                             + size(current_ix + 1, flexes.as_slice())
-    //                             - next_target_size,
-    //                         min_size,
-    //                     );
-
-    //                     let current_pixel_change =
-    //                         current_target_size - size(current_ix, flexes.as_slice());
-
-    //                     let (current_target_flex, next_target_flex) =
-    //                         flex_changes(current_pixel_change, current_ix, 1, flexes.as_slice());
-
-    //                     flexes[current_ix] = current_target_flex;
-    //                     flexes[current_ix + 1] = next_target_flex;
-
-    //                     proposed_current_pixel_change -= current_pixel_change;
-    //                 }
-
-    //                 workspace.schedule_serialize(cx);
-    //                 cx.notify();
-    //             }
-    //         }
-    //     }
-
-    //     impl Extend<AnyElement<Workspace>> for PaneAxisElement {
-    //         fn extend<T: IntoIterator<Item = AnyElement<Workspace>>>(&mut self, children: T) {
-    //             self.children.extend(children);
-    //         }
-    //     }
-
-    //     impl Element<Workspace> for PaneAxisElement {
-    //         type LayoutState = f32;
-    //         type PaintState = ();
-
-    //         fn layout(
-    //             &mut self,
-    //             constraint: SizeConstraint,
-    //             view: &mut Workspace,
-    //             cx: &mut ViewContext<Workspace>,
-    //         ) -> (Vector2F, Self::LayoutState) {
-    //             debug_assert!(self.children.len() == self.flexes.borrow().len());
-
-    //             let active_pane_magnification =
-    //                 settings::get::<WorkspaceSettings>(cx).active_pane_magnification;
-
-    //             let mut remaining_flex = 0.;
-
-    //             if active_pane_magnification != 1. {
-    //                 let active_pane_flex = self
-    //                     .active_pane_ix
-    //                     .map(|_| active_pane_magnification)
-    //                     .unwrap_or(1.);
-    //                 remaining_flex += self.children.len() as f32 - 1. + active_pane_flex;
-    //             } else {
-    //                 for flex in self.flexes.borrow().iter() {
-    //                     remaining_flex += flex;
-    //                 }
-    //             }
-
-    //             let mut cross_axis_max: f32 = 0.0;
-    //             let mut remaining_space = constraint.max_along(self.axis);
-
-    //             if remaining_space.is_infinite() {
-    //                 panic!("flex contains flexible children but has an infinite constraint along the flex axis");
-    //             }
-
-    //             self.layout_children(
-    //                 active_pane_magnification,
-    //                 constraint,
-    //                 &mut remaining_space,
-    //                 &mut remaining_flex,
-    //                 &mut cross_axis_max,
-    //                 view,
-    //                 cx,
-    //             );
-
-    //             let mut size = match self.axis {
-    //                 Axis::Horizontal => vec2f(constraint.max.x() - remaining_space, cross_axis_max),
-    //                 Axis::Vertical => vec2f(cross_axis_max, constraint.max.y() - remaining_space),
-    //             };
-
-    //             if constraint.min.x().is_finite() {
-    //                 size.set_x(size.x().max(constraint.min.x()));
-    //             }
-    //             if constraint.min.y().is_finite() {
-    //                 size.set_y(size.y().max(constraint.min.y()));
-    //             }
-
-    //             if size.x() > constraint.max.x() {
-    //                 size.set_x(constraint.max.x());
-    //             }
-    //             if size.y() > constraint.max.y() {
-    //                 size.set_y(constraint.max.y());
-    //             }
-
-    //             (size, remaining_space)
-    //         }
-
-    //         fn paint(
-    //             &mut self,
-    //             bounds: Bounds<Pixels>,
-    //             visible_bounds: Bounds<Pixels>,
-    //             remaining_space: &mut Self::LayoutState,
-    //             view: &mut Workspace,
-    //             cx: &mut ViewContext<Workspace>,
-    //         ) -> Self::PaintState {
-    //             let can_resize = settings::get::<WorkspaceSettings>(cx).active_pane_magnification == 1.;
-    //             let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
-
-    //             let overflowing = *remaining_space < 0.;
-    //             if overflowing {
-    //                 cx.scene().push_layer(Some(visible_bounds));
-    //             }
-
-    //             let mut child_origin = bounds.origin();
-
-    //             let mut bounding_boxes = self.bounding_boxes.borrow_mut();
-    //             bounding_boxes.clear();
-
-    //             let mut children_iter = self.children.iter_mut().enumerate().peekable();
-    //             while let Some((ix, child)) = children_iter.next() {
-    //                 let child_start = child_origin.clone();
-    //                 child.paint(child_origin, visible_bounds, view, cx);
-
-    //                 bounding_boxes.push(Some(Bounds<Pixels>::new(child_origin, child.size())));
-
-    //                 match self.axis {
-    //                     Axis::Horizontal => child_origin += vec2f(child.size().x(), 0.0),
-    //                     Axis::Vertical => child_origin += vec2f(0.0, child.size().y()),
-    //                 }
-
-    //                 if can_resize && children_iter.peek().is_some() {
-    //                     cx.scene().push_stacking_context(None, None);
-
-    //                     let handle_origin = match self.axis {
-    //                         Axis::Horizontal => child_origin - vec2f(HANDLE_HITBOX_SIZE / 2., 0.0),
-    //                         Axis::Vertical => child_origin - vec2f(0.0, HANDLE_HITBOX_SIZE / 2.),
-    //                     };
-
-    //                     let handle_bounds = match self.axis {
-    //                         Axis::Horizontal => Bounds<Pixels>::new(
-    //                             handle_origin,
-    //                             vec2f(HANDLE_HITBOX_SIZE, visible_bounds.height()),
-    //                         ),
-    //                         Axis::Vertical => Bounds<Pixels>::new(
-    //                             handle_origin,
-    //                             vec2f(visible_bounds.width(), HANDLE_HITBOX_SIZE),
-    //                         ),
-    //                     };
-
-    //                     let style = match self.axis {
-    //                         Axis::Horizontal => CursorStyle::ResizeLeftRight,
-    //                         Axis::Vertical => CursorStyle::ResizeUpDown,
-    //                     };
-
-    //                     cx.scene().push_cursor_region(CursorRegion {
-    //                         bounds: handle_bounds,
-    //                         style,
-    //                     });
-
-    //                     enum ResizeHandle {}
-    //                     let mut mouse_region = MouseRegion::new::<ResizeHandle>(
-    //                         cx.view_id(),
-    //                         self.basis + ix,
-    //                         handle_bounds,
-    //                     );
-    //                     mouse_region = mouse_region
-    //                         .on_drag(
-    //                             MouseButton::Left,
-    //                             Self::handle_resize(
-    //                                 self.flexes.clone(),
-    //                                 self.axis,
-    //                                 ix,
-    //                                 child_start,
-    //                                 visible_bounds.clone(),
-    //                             ),
-    //                         )
-    //                         .on_click(MouseButton::Left, {
-    //                             let flexes = self.flexes.clone();
-    //                             move |e, v: &mut Workspace, cx| {
-    //                                 if e.click_count >= 2 {
-    //                                     let mut borrow = flexes.borrow_mut();
-    //                                     *borrow = vec![1.; borrow.len()];
-    //                                     v.schedule_serialize(cx);
-    //                                     cx.notify();
-    //                                 }
-    //                             }
-    //                         });
-    //                     cx.scene().push_mouse_region(mouse_region);
-
-    //                     cx.scene().pop_stacking_context();
-    //                 }
-    //             }
-
-    //             if overflowing {
-    //                 cx.scene().pop_layer();
-    //             }
-    //         }
-
-    //         fn rect_for_text_range(
-    //             &self,
-    //             range_utf16: Range<usize>,
-    //             _: Bounds<Pixels>,
-    //             _: Bounds<Pixels>,
-    //             _: &Self::LayoutState,
-    //             _: &Self::PaintState,
-    //             view: &Workspace,
-    //             cx: &ViewContext<Workspace>,
-    //         ) -> Option<Bounds<Pixels>> {
-    //             self.children
-    //                 .iter()
-    //                 .find_map(|child| child.rect_for_text_range(range_utf16.clone(), view, cx))
-    //         }
-
-    //         fn debug(
-    //             &self,
-    //             bounds: Bounds<Pixels>,
-    //             _: &Self::LayoutState,
-    //             _: &Self::PaintState,
-    //             view: &Workspace,
-    //             cx: &ViewContext<Workspace>,
-    //         ) -> json::Value {
-    //             serde_json::json!({
-    //                 "type": "PaneAxis",
-    //                 "bounds": bounds.to_json(),
-    //                 "axis": self.axis.to_json(),
-    //                 "flexes": *self.flexes.borrow(),
-    //                 "children": self.children.iter().map(|child| child.debug(view, cx)).collect::<Vec<json::Value>>()
-    //             })
-    //         }
-    //     }
 }

crates/workspace2/src/searchable.rs πŸ”—

@@ -193,7 +193,7 @@ impl<T: SearchableItem> SearchableItemHandle for View<T> {
         cx: &mut WindowContext,
     ) -> Task<Vec<Box<dyn Any + Send>>> {
         let matches = self.update(cx, |this, cx| this.find_matches(query, cx));
-        cx.spawn(|cx| async {
+        cx.spawn(|_| async {
             let matches = matches.await;
             matches
                 .into_iter()
@@ -253,7 +253,7 @@ pub trait WeakSearchableItemHandle: WeakItemHandle {
 }
 
 impl<T: SearchableItem> WeakSearchableItemHandle for WeakView<T> {
-    fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>> {
+    fn upgrade(&self, _cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>> {
         Some(Box::new(self.upgrade()?))
     }
 

crates/workspace2/src/status_bar.rs πŸ”—

@@ -51,14 +51,14 @@ impl Render for StatusBar {
 }
 
 impl StatusBar {
-    fn render_left_tools(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+    fn render_left_tools(&self, _: &mut ViewContext<Self>) -> impl IntoElement {
         h_stack()
             .items_center()
             .gap_2()
             .children(self.left_items.iter().map(|item| item.to_any()))
     }
 
-    fn render_right_tools(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+    fn render_right_tools(&self, _: &mut ViewContext<Self>) -> impl IntoElement {
         h_stack()
             .items_center()
             .gap_2()

crates/workspace2/src/toolbar.rs πŸ”—

@@ -55,6 +55,12 @@ pub struct Toolbar {
 }
 
 impl Toolbar {
+    fn has_any_visible_items(&self) -> bool {
+        self.items
+            .iter()
+            .any(|(_item, location)| *location != ToolbarItemLocation::Hidden)
+    }
+
     fn left_items(&self) -> impl Iterator<Item = &dyn ToolbarItemViewHandle> {
         self.items.iter().filter_map(|(item, location)| {
             if *location == ToolbarItemLocation::PrimaryLeft {
@@ -90,6 +96,10 @@ impl Render for Toolbar {
     type Element = Div;
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+        if !self.has_any_visible_items() {
+            return div();
+        }
+
         let secondary_item = self.secondary_items().next().map(|item| item.to_any());
 
         v_stack()
@@ -99,8 +109,22 @@ impl Render for Toolbar {
             .child(
                 h_stack()
                     .justify_between()
-                    .child(h_stack().children(self.left_items().map(|item| item.to_any())))
-                    .child(h_stack().children(self.right_items().map(|item| item.to_any()))),
+                    .when(self.left_items().count() > 0, |this| {
+                        this.child(
+                            h_stack()
+                                .flex_1()
+                                .justify_start()
+                                .children(self.left_items().map(|item| item.to_any())),
+                        )
+                    })
+                    .when(self.right_items().count() > 0, |this| {
+                        this.child(
+                            h_stack()
+                                .flex_1()
+                                .justify_end()
+                                .children(self.right_items().map(|item| item.to_any())),
+                        )
+                    }),
             )
             .children(secondary_item)
     }

crates/workspace2/src/workspace2.rs πŸ”—

@@ -1,15 +1,11 @@
-#![allow(unused_variables, dead_code, unused_mut)]
-// todo!() this is to make transition easier.
-
 pub mod dock;
 pub mod item;
+mod modal_layer;
 pub mod notifications;
 pub mod pane;
 pub mod pane_group;
 mod persistence;
 pub mod searchable;
-// todo!()
-mod modal_layer;
 pub mod shared_screen;
 mod status_bar;
 mod toolbar;
@@ -30,11 +26,11 @@ use futures::{
 };
 use gpui::{
     actions, canvas, div, impl_actions, point, size, Action, AnyModel, AnyView, AnyWeakView,
-    AnyWindowHandle, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Context, Div, Entity,
-    EntityId, EventEmitter, FocusHandle, FocusableView, GlobalPixels, InteractiveElement,
-    KeyContext, ManagedView, Model, ModelContext, MouseMoveEvent, ParentElement, PathPromptOptions,
-    Pixels, Point, PromptLevel, Render, Size, Styled, Subscription, Task, View, ViewContext,
-    VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions,
+    AnyWindowHandle, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Context, Div,
+    DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle, FocusableView, GlobalPixels,
+    InteractiveElement, KeyContext, ManagedView, Model, ModelContext, ParentElement,
+    PathPromptOptions, Pixels, Point, PromptLevel, Render, Size, Styled, Subscription, Task, View,
+    ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions,
 };
 use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
 use itertools::Itertools;
@@ -227,9 +223,6 @@ pub fn init_settings(cx: &mut AppContext) {
 }
 
 pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
-    cx.default_global::<DockDragState>();
-    cx.default_global::<DockClickReset>();
-
     init_settings(cx);
     notifications::init(cx);
 
@@ -239,14 +232,14 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
     cx.on_action({
         let app_state = Arc::downgrade(&app_state);
         move |_: &Open, cx: &mut AppContext| {
-            let mut paths = cx.prompt_for_paths(PathPromptOptions {
+            let paths = cx.prompt_for_paths(PathPromptOptions {
                 files: true,
                 directories: true,
                 multiple: true,
             });
 
             if let Some(app_state) = app_state.upgrade() {
-                cx.spawn(move |mut cx| async move {
+                cx.spawn(move |cx| async move {
                     if let Some(paths) = paths.await.log_err().flatten() {
                         cx.update(|cx| {
                             open_paths(&paths, &app_state, None, cx).detach_and_log_err(cx)
@@ -461,11 +454,12 @@ pub struct Workspace {
     leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
     database_id: WorkspaceId,
     app_state: Arc<AppState>,
-    subscriptions: Vec<Subscription>,
+    _subscriptions: Vec<Subscription>,
     _apply_leader_updates: Task<Result<()>>,
     _observe_current_user: Task<Result<()>>,
     _schedule_serialize: Option<Task<()>>,
     pane_history_timestamp: Arc<AtomicUsize>,
+    bounds: Bounds<Pixels>,
 }
 
 impl EventEmitter<Event> for Workspace {}
@@ -592,12 +586,9 @@ impl Workspace {
         let left_dock = cx.build_view(|cx| Dock::new(DockPosition::Left, cx));
         let bottom_dock = cx.build_view(|cx| Dock::new(DockPosition::Bottom, cx));
         let right_dock = cx.build_view(|cx| Dock::new(DockPosition::Right, cx));
-        let left_dock_buttons =
-            cx.build_view(|cx| PanelButtons::new(left_dock.clone(), weak_handle.clone(), cx));
-        let bottom_dock_buttons =
-            cx.build_view(|cx| PanelButtons::new(bottom_dock.clone(), weak_handle.clone(), cx));
-        let right_dock_buttons =
-            cx.build_view(|cx| PanelButtons::new(right_dock.clone(), weak_handle.clone(), cx));
+        let left_dock_buttons = cx.build_view(|cx| PanelButtons::new(left_dock.clone(), cx));
+        let bottom_dock_buttons = cx.build_view(|cx| PanelButtons::new(bottom_dock.clone(), cx));
+        let right_dock_buttons = cx.build_view(|cx| PanelButtons::new(right_dock.clone(), cx));
         let status_bar = cx.build_view(|cx| {
             let mut status_bar = StatusBar::new(&center_pane.clone(), cx);
             status_bar.add_left_item(left_dock_buttons, cx);
@@ -606,8 +597,7 @@ impl Workspace {
             status_bar
         });
 
-        let workspace_handle = cx.view().downgrade();
-        let modal_layer = cx.build_view(|cx| ModalLayer::new());
+        let modal_layer = cx.build_view(|_| ModalLayer::new());
 
         // todo!()
         // cx.update_default_global::<DragAndDrop<Workspace>, _, _>(|drag_and_drop, _| {
@@ -705,9 +695,11 @@ impl Workspace {
             _apply_leader_updates,
             _schedule_serialize: None,
             leader_updates_tx,
-            subscriptions,
+            _subscriptions: subscriptions,
             pane_history_timestamp,
             workspace_actions: Default::default(),
+            // This data will be incorrect, but it will be overwritten by the time it needs to be used.
+            bounds: Default::default(),
         }
     }
 
@@ -763,7 +755,7 @@ impl Workspace {
             };
 
             let window = if let Some(window) = requesting_window {
-                cx.update_window(window.into(), |old_workspace, cx| {
+                cx.update_window(window.into(), |_, cx| {
                     cx.replace_root_view(|cx| {
                         Workspace::new(workspace_id, project_handle.clone(), app_state.clone(), cx)
                     });
@@ -1145,7 +1137,7 @@ impl Workspace {
         let window = cx.window_handle();
 
         cx.spawn(|this, mut cx| async move {
-            let workspace_count = cx.update(|_, cx| {
+            let workspace_count = (*cx).update(|cx| {
                 cx.windows()
                     .iter()
                     .filter(|window| window.downcast::<Workspace>().is_some())
@@ -1185,8 +1177,7 @@ impl Workspace {
     }
 
     fn save_all(&mut self, action: &SaveAll, cx: &mut ViewContext<Self>) {
-        let save_all = self
-            .save_all_internal(action.save_intent.unwrap_or(SaveIntent::SaveAll), cx)
+        self.save_all_internal(action.save_intent.unwrap_or(SaveIntent::SaveAll), cx)
             .detach_and_log_err(cx);
     }
 
@@ -1216,7 +1207,7 @@ impl Workspace {
         cx.spawn(|workspace, mut cx| async move {
             // Override save mode and display "Save all files" prompt
             if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
-                let mut answer = workspace.update(&mut cx, |_, cx| {
+                let answer = workspace.update(&mut cx, |_, cx| {
                     let prompt = Pane::file_names_for_prompt(
                         &mut dirty_items.iter().map(|(_, handle)| handle),
                         dirty_items.len(),
@@ -1261,7 +1252,7 @@ impl Workspace {
     }
 
     pub fn open(&mut self, _: &Open, cx: &mut ViewContext<Self>) {
-        let mut paths = cx.prompt_for_paths(PathPromptOptions {
+        let paths = cx.prompt_for_paths(PathPromptOptions {
             files: true,
             directories: true,
             multiple: true,
@@ -1390,7 +1381,7 @@ impl Workspace {
     }
 
     fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext<Self>) {
-        let mut paths = cx.prompt_for_paths(PathPromptOptions {
+        let paths = cx.prompt_for_paths(PathPromptOptions {
             files: false,
             directories: true,
             multiple: true,
@@ -1670,6 +1661,8 @@ impl Workspace {
         None
     }
 
+    // todo!("implement zoom")
+    #[allow(unused)]
     fn zoom_out(&mut self, cx: &mut ViewContext<Self>) {
         for pane in &self.panes {
             pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
@@ -2043,20 +2036,20 @@ impl Workspace {
             _ => bounding_box.center(),
         };
 
-        let distance_to_next = 1.; //todo(pane dividers styling)
+        let distance_to_next = 8.; //todo(pane dividers styling)
 
         let target = match direction {
             SplitDirection::Left => {
-                Point::new(bounding_box.origin.x - distance_to_next.into(), center.y)
+                Point::new(bounding_box.left() - distance_to_next.into(), center.y)
             }
             SplitDirection::Right => {
                 Point::new(bounding_box.right() + distance_to_next.into(), center.y)
             }
             SplitDirection::Up => {
-                Point::new(center.x, bounding_box.origin.y - distance_to_next.into())
+                Point::new(center.x, bounding_box.top() - distance_to_next.into())
             }
             SplitDirection::Down => {
-                Point::new(center.x, bounding_box.top() + distance_to_next.into())
+                Point::new(center.x, bounding_box.bottom() + distance_to_next.into())
             }
         };
         self.center.pane_at_pixel_position(target)
@@ -2574,7 +2567,7 @@ impl Workspace {
     //         }
     //     }
 
-    fn render_notifications(&self, cx: &ViewContext<Self>) -> Option<Div> {
+    fn render_notifications(&self, _cx: &ViewContext<Self>) -> Option<Div> {
         if self.notifications.is_empty() {
             None
         } else {
@@ -3005,6 +2998,7 @@ impl Workspace {
         cx.notify();
     }
 
+    #[allow(unused)]
     fn schedule_serialize(&mut self, cx: &mut ViewContext<Self>) {
         self._schedule_serialize = Some(cx.spawn(|this, mut cx| async move {
             cx.background_executor()
@@ -3143,12 +3137,7 @@ impl Workspace {
         cx: &mut ViewContext<Workspace>,
     ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
         cx.spawn(|workspace, mut cx| async move {
-            let (project, old_center_pane) = workspace.update(&mut cx, |workspace, _| {
-                (
-                    workspace.project().clone(),
-                    workspace.last_active_center_pane.clone(),
-                )
-            })?;
+            let project = workspace.update(&mut cx, |workspace, _| workspace.project().clone())?;
 
             let mut center_group = None;
             let mut center_items = None;
@@ -3293,7 +3282,7 @@ impl Workspace {
             .on_action(cx.listener(|workspace, action: &SwapPaneInDirection, cx| {
                 workspace.swap_pane_in_direction(action.0, cx)
             }))
-            .on_action(cx.listener(|this, e: &ToggleLeftDock, cx| {
+            .on_action(cx.listener(|this, _: &ToggleLeftDock, cx| {
                 this.toggle_dock(DockPosition::Left, cx);
             }))
             .on_action(
@@ -3313,42 +3302,12 @@ impl Workspace {
             )
             .on_action(cx.listener(Workspace::open))
             .on_action(cx.listener(Workspace::close_window))
-
-        //     cx.add_action(Workspace::activate_pane_at_index);
-        //     cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
-        //         workspace.reopen_closed_item(cx).detach();
-        //     });
-        //     cx.add_action(|workspace: &mut Workspace, _: &GoBack, cx| {
-        //         workspace
-        //             .go_back(workspace.active_pane().downgrade(), cx)
-        //             .detach();
-        //     });
-        //     cx.add_action(|workspace: &mut Workspace, _: &GoForward, cx| {
-        //         workspace
-        //             .go_forward(workspace.active_pane().downgrade(), cx)
-        //             .detach();
-        //     });
-
-        //     cx.add_action(|_: &mut Workspace, _: &install_cli::Install, cx| {
-        //         cx.spawn(|workspace, mut cx| async move {
-        //             let err = install_cli::install_cli(&cx)
-        //                 .await
-        //                 .context("Failed to create CLI symlink");
-
-        //             workspace.update(&mut cx, |workspace, cx| {
-        //                 if matches!(err, Err(_)) {
-        //                     err.notify_err(workspace, cx);
-        //                 } else {
-        //                     workspace.show_notification(1, cx, |cx| {
-        //                         cx.build_view(|_| {
-        //                             MessageNotification::new("Successfully installed the `zed` binary")
-        //                         })
-        //                     });
-        //                 }
-        //             })
-        //         })
-        //         .detach();
-        //     });
+            .on_action(cx.listener(Workspace::activate_pane_at_index))
+            .on_action(
+                cx.listener(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
+                    workspace.reopen_closed_item(cx).detach();
+                }),
+            )
     }
 
     #[cfg(any(test, feature = "test-support"))]
@@ -3418,7 +3377,7 @@ impl Workspace {
         self
     }
 
-    fn add_workspace_actions_listeners(&self, mut div: Div, cx: &mut ViewContext<Self>) -> Div {
+    fn add_workspace_actions_listeners(&self, div: Div, cx: &mut ViewContext<Self>) -> Div {
         let mut div = div
             .on_action(cx.listener(Self::close_inactive_items_and_panes))
             .on_action(cx.listener(Self::close_all_items_and_panes))
@@ -3578,15 +3537,8 @@ impl FocusableView for Workspace {
     }
 }
 
-struct WorkspaceBounds(Bounds<Pixels>);
-
-//todo!("remove this when better drag APIs are in GPUI2")
-#[derive(Default)]
-struct DockDragState(Option<DockPosition>);
-
-//todo!("remove this when better double APIs are in GPUI2")
-#[derive(Default)]
-struct DockClickReset(Option<Task<()>>);
+#[derive(Clone, Render)]
+struct DraggedDock(DockPosition);
 
 impl Render for Workspace {
     type Element = Div;
@@ -3632,37 +3584,37 @@ impl Render for Workspace {
                     .border_t()
                     .border_b()
                     .border_color(cx.theme().colors().border)
-                    .on_mouse_up(gpui::MouseButton::Left, |_, cx| {
-                        cx.update_global(|drag: &mut DockDragState, cx| {
-                            drag.0 = None;
-                        })
-                    })
-                    .on_mouse_move(cx.listener(|workspace, e: &MouseMoveEvent, cx| {
-                        if let Some(types) = &cx.global::<DockDragState>().0 {
-                            let workspace_bounds = cx.global::<WorkspaceBounds>().0;
-                            match types {
+                    .child(
+                        canvas(cx.listener(|workspace, bounds, _| {
+                            workspace.bounds = *bounds;
+                        }))
+                        .absolute()
+                        .size_full(),
+                    )
+                    .on_drag_move(
+                        cx.listener(|workspace, e: &DragMoveEvent<DraggedDock>, cx| {
+                            match e.drag(cx).0 {
                                 DockPosition::Left => {
-                                    let size = e.position.x;
+                                    let size = workspace.bounds.left() + e.event.position.x;
                                     workspace.left_dock.update(cx, |left_dock, cx| {
                                         left_dock.resize_active_panel(Some(size.0), cx);
                                     });
                                 }
                                 DockPosition::Right => {
-                                    let size = workspace_bounds.size.width - e.position.x;
+                                    let size = workspace.bounds.right() - e.event.position.x;
                                     workspace.right_dock.update(cx, |right_dock, cx| {
                                         right_dock.resize_active_panel(Some(size.0), cx);
                                     });
                                 }
                                 DockPosition::Bottom => {
-                                    let size = workspace_bounds.size.height - e.position.y;
+                                    let size = workspace.bounds.bottom() - e.event.position.y;
                                     workspace.bottom_dock.update(cx, |bottom_dock, cx| {
                                         bottom_dock.resize_active_panel(Some(size.0), cx);
                                     });
                                 }
                             }
-                        }
-                    }))
-                    .child(canvas(|bounds, cx| cx.set_global(WorkspaceBounds(bounds))))
+                        }),
+                    )
                     .child(self.modal_layer.clone())
                     .child(
                         div()
@@ -4061,7 +4013,7 @@ async fn join_channel_internal(
     active_call: &Model<ActiveCall>,
     cx: &mut AsyncAppContext,
 ) -> Result<bool> {
-    let (should_prompt, open_room) = active_call.read_with(cx, |active_call, cx| {
+    let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
         let Some(room) = active_call.room().map(|room| room.read(cx)) else {
             return (false, None);
         };
@@ -4426,7 +4378,7 @@ pub fn restart(_: &Restart, cx: &mut AppContext) {
     }
 
     cx.spawn(|mut cx| async move {
-        if let Some(mut prompt) = prompt {
+        if let Some(prompt) = prompt {
             let answer = prompt.await?;
             if answer != 0 {
                 return Ok(());
@@ -4465,6 +4417,8 @@ fn parse_pixel_size_env_var(value: &str) -> Option<Size<GlobalPixels>> {
 
 #[cfg(test)]
 mod tests {
+    use std::{cell::RefCell, rc::Rc};
+
     use super::*;
     use crate::item::{
         test::{TestItem, TestProjectItem},
@@ -4475,7 +4429,6 @@ mod tests {
     use project::{Project, ProjectEntryId};
     use serde_json::json;
     use settings::SettingsStore;
-    use std::{cell::RefCell, rc::Rc};
 
     #[gpui::test]
     async fn test_tab_disambiguation(cx: &mut TestAppContext) {
@@ -4549,7 +4502,7 @@ mod tests {
         let project = Project::test(fs, ["root1".as_ref()], cx).await;
         let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
         let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
-        let worktree_id = project.read_with(cx, |project, cx| {
+        let worktree_id = project.update(cx, |project, cx| {
             project.worktrees().next().unwrap().read(cx).id()
         });
 
@@ -4562,7 +4515,7 @@ mod tests {
 
         // Add an item to an empty pane
         workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item1), cx));
-        project.read_with(cx, |project, cx| {
+        project.update(cx, |project, cx| {
             assert_eq!(
                 project.active_entry(),
                 project
@@ -4575,7 +4528,7 @@ mod tests {
         // Add a second item to a non-empty pane
         workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx));
         assert_eq!(cx.window_title().as_deref(), Some("two.txt β€” root1"));
-        project.read_with(cx, |project, cx| {
+        project.update(cx, |project, cx| {
             assert_eq!(
                 project.active_entry(),
                 project
@@ -4591,7 +4544,7 @@ mod tests {
         .await
         .unwrap();
         assert_eq!(cx.window_title().as_deref(), Some("one.txt β€” root1"));
-        project.read_with(cx, |project, cx| {
+        project.update(cx, |project, cx| {
             assert_eq!(
                 project.active_entry(),
                 project
@@ -4975,14 +4928,14 @@ mod tests {
             item.is_dirty = true;
             cx.blur();
         });
-        cx.executor().run_until_parked();
+        cx.run_until_parked();
         item.update(cx, |item, _| assert_eq!(item.save_count, 5));
 
         // Ensure autosave is prevented for deleted files also when closing the buffer.
         let _close_items = pane.update(cx, |pane, cx| {
             pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
         });
-        cx.executor().run_until_parked();
+        cx.run_until_parked();
         assert!(cx.has_pending_prompt());
         item.update(cx, |item, _| assert_eq!(item.save_count, 5));
     }
@@ -5041,363 +4994,362 @@ mod tests {
         });
     }
 
-    //     #[gpui::test]
-    //     async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
-    //         init_test(cx);
-    //         let fs = FakeFs::new(cx.executor());
-
-    //         let project = Project::test(fs, [], cx).await;
-    //         let window = cx.add_window(|cx| Workspace::test_new(project, cx));
-    //         let workspace = window.root(cx);
-
-    //         let panel = workspace.update(cx, |workspace, cx| {
-    //             let panel = cx.build_view(|_| TestPanel::new(DockPosition::Right));
-    //             workspace.add_panel(panel.clone(), cx);
-
-    //             workspace
-    //                 .right_dock()
-    //                 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
-
-    //             panel
-    //         });
-
-    //         let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
-    //         pane.update(cx, |pane, cx| {
-    //             let item = cx.build_view(|_| TestItem::new(cx));
-    //             pane.add_item(Box::new(item), true, true, None, cx);
-    //         });
-
-    //         // Transfer focus from center to panel
-    //         workspace.update(cx, |workspace, cx| {
-    //             workspace.toggle_panel_focus::<TestPanel>(cx);
-    //         });
-
-    //         workspace.update(cx, |workspace, cx| {
-    //             assert!(workspace.right_dock().read(cx).is_open());
-    //             assert!(!panel.is_zoomed(cx));
-    //             assert!(panel.has_focus(cx));
-    //         });
-
-    //         // Transfer focus from panel to center
-    //         workspace.update(cx, |workspace, cx| {
-    //             workspace.toggle_panel_focus::<TestPanel>(cx);
-    //         });
-
-    //         workspace.update(cx, |workspace, cx| {
-    //             assert!(workspace.right_dock().read(cx).is_open());
-    //             assert!(!panel.is_zoomed(cx));
-    //             assert!(!panel.has_focus(cx));
-    //         });
-
-    //         // Close the dock
-    //         workspace.update(cx, |workspace, cx| {
-    //             workspace.toggle_dock(DockPosition::Right, cx);
-    //         });
-
-    //         workspace.update(cx, |workspace, cx| {
-    //             assert!(!workspace.right_dock().read(cx).is_open());
-    //             assert!(!panel.is_zoomed(cx));
-    //             assert!(!panel.has_focus(cx));
-    //         });
-
-    //         // Open the dock
-    //         workspace.update(cx, |workspace, cx| {
-    //             workspace.toggle_dock(DockPosition::Right, cx);
-    //         });
-
-    //         workspace.update(cx, |workspace, cx| {
-    //             assert!(workspace.right_dock().read(cx).is_open());
-    //             assert!(!panel.is_zoomed(cx));
-    //             assert!(panel.has_focus(cx));
-    //         });
-
-    //         // Focus and zoom panel
-    //         panel.update(cx, |panel, cx| {
-    //             cx.focus_self();
-    //             panel.set_zoomed(true, cx)
-    //         });
-
-    //         workspace.update(cx, |workspace, cx| {
-    //             assert!(workspace.right_dock().read(cx).is_open());
-    //             assert!(panel.is_zoomed(cx));
-    //             assert!(panel.has_focus(cx));
-    //         });
-
-    //         // Transfer focus to the center closes the dock
-    //         workspace.update(cx, |workspace, cx| {
-    //             workspace.toggle_panel_focus::<TestPanel>(cx);
-    //         });
-
-    //         workspace.update(cx, |workspace, cx| {
-    //             assert!(!workspace.right_dock().read(cx).is_open());
-    //             assert!(panel.is_zoomed(cx));
-    //             assert!(!panel.has_focus(cx));
-    //         });
-
-    //         // Transferring focus back to the panel keeps it zoomed
-    //         workspace.update(cx, |workspace, cx| {
-    //             workspace.toggle_panel_focus::<TestPanel>(cx);
-    //         });
-
-    //         workspace.update(cx, |workspace, cx| {
-    //             assert!(workspace.right_dock().read(cx).is_open());
-    //             assert!(panel.is_zoomed(cx));
-    //             assert!(panel.has_focus(cx));
-    //         });
-
-    //         // Close the dock while it is zoomed
-    //         workspace.update(cx, |workspace, cx| {
-    //             workspace.toggle_dock(DockPosition::Right, cx)
-    //         });
-
-    //         workspace.update(cx, |workspace, cx| {
-    //             assert!(!workspace.right_dock().read(cx).is_open());
-    //             assert!(panel.is_zoomed(cx));
-    //             assert!(workspace.zoomed.is_none());
-    //             assert!(!panel.has_focus(cx));
-    //         });
-
-    //         // Opening the dock, when it's zoomed, retains focus
-    //         workspace.update(cx, |workspace, cx| {
-    //             workspace.toggle_dock(DockPosition::Right, cx)
-    //         });
-
-    //         workspace.update(cx, |workspace, cx| {
-    //             assert!(workspace.right_dock().read(cx).is_open());
-    //             assert!(panel.is_zoomed(cx));
-    //             assert!(workspace.zoomed.is_some());
-    //             assert!(panel.has_focus(cx));
-    //         });
-
-    //         // Unzoom and close the panel, zoom the active pane.
-    //         panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
-    //         workspace.update(cx, |workspace, cx| {
-    //             workspace.toggle_dock(DockPosition::Right, cx)
-    //         });
-    //         pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
+    // #[gpui::test]
+    // async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
+    //     init_test(cx);
+    //     let fs = FakeFs::new(cx.executor());
+
+    //     let project = Project::test(fs, [], cx).await;
+    //     let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
+
+    //     let panel = workspace.update(cx, |workspace, cx| {
+    //         let panel = cx.build_view(|cx| TestPanel::new(DockPosition::Right, cx));
+    //         workspace.add_panel(panel.clone(), cx);
+
+    //         workspace
+    //             .right_dock()
+    //             .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
+
+    //         panel
+    //     });
+
+    //     let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
+    //     pane.update(cx, |pane, cx| {
+    //         let item = cx.build_view(|cx| TestItem::new(cx));
+    //         pane.add_item(Box::new(item), true, true, None, cx);
+    //     });
+
+    //     // Transfer focus from center to panel
+    //     workspace.update(cx, |workspace, cx| {
+    //         workspace.toggle_panel_focus::<TestPanel>(cx);
+    //     });
+
+    //     workspace.update(cx, |workspace, cx| {
+    //         assert!(workspace.right_dock().read(cx).is_open());
+    //         assert!(!panel.is_zoomed(cx));
+    //         assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
+    //     });
+
+    //     // Transfer focus from panel to center
+    //     workspace.update(cx, |workspace, cx| {
+    //         workspace.toggle_panel_focus::<TestPanel>(cx);
+    //     });
+
+    //     workspace.update(cx, |workspace, cx| {
+    //         assert!(workspace.right_dock().read(cx).is_open());
+    //         assert!(!panel.is_zoomed(cx));
+    //         assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
+    //     });
+
+    //     // Close the dock
+    //     workspace.update(cx, |workspace, cx| {
+    //         workspace.toggle_dock(DockPosition::Right, cx);
+    //     });
+
+    //     workspace.update(cx, |workspace, cx| {
+    //         assert!(!workspace.right_dock().read(cx).is_open());
+    //         assert!(!panel.is_zoomed(cx));
+    //         assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
+    //     });
+
+    //     // Open the dock
+    //     workspace.update(cx, |workspace, cx| {
+    //         workspace.toggle_dock(DockPosition::Right, cx);
+    //     });
+
+    //     workspace.update(cx, |workspace, cx| {
+    //         assert!(workspace.right_dock().read(cx).is_open());
+    //         assert!(!panel.is_zoomed(cx));
+    //         assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
+    //     });
+
+    //     // Focus and zoom panel
+    //     panel.update(cx, |panel, cx| {
+    //         cx.focus_self();
+    //         panel.set_zoomed(true, cx)
+    //     });
+
+    //     workspace.update(cx, |workspace, cx| {
+    //         assert!(workspace.right_dock().read(cx).is_open());
+    //         assert!(panel.is_zoomed(cx));
+    //         assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
+    //     });
+
+    //     // Transfer focus to the center closes the dock
+    //     workspace.update(cx, |workspace, cx| {
+    //         workspace.toggle_panel_focus::<TestPanel>(cx);
+    //     });
+
+    //     workspace.update(cx, |workspace, cx| {
+    //         assert!(!workspace.right_dock().read(cx).is_open());
+    //         assert!(panel.is_zoomed(cx));
+    //         assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
+    //     });
+
+    //     // Transferring focus back to the panel keeps it zoomed
+    //     workspace.update(cx, |workspace, cx| {
+    //         workspace.toggle_panel_focus::<TestPanel>(cx);
+    //     });
+
+    //     workspace.update(cx, |workspace, cx| {
+    //         assert!(workspace.right_dock().read(cx).is_open());
+    //         assert!(panel.is_zoomed(cx));
+    //         assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
+    //     });
+
+    //     // Close the dock while it is zoomed
+    //     workspace.update(cx, |workspace, cx| {
+    //         workspace.toggle_dock(DockPosition::Right, cx)
+    //     });
+
+    //     workspace.update(cx, |workspace, cx| {
+    //         assert!(!workspace.right_dock().read(cx).is_open());
+    //         assert!(panel.is_zoomed(cx));
+    //         assert!(workspace.zoomed.is_none());
+    //         assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
+    //     });
+
+    //     // Opening the dock, when it's zoomed, retains focus
+    //     workspace.update(cx, |workspace, cx| {
+    //         workspace.toggle_dock(DockPosition::Right, cx)
+    //     });
+
+    //     workspace.update(cx, |workspace, cx| {
+    //         assert!(workspace.right_dock().read(cx).is_open());
+    //         assert!(panel.is_zoomed(cx));
+    //         assert!(workspace.zoomed.is_some());
+    //         assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
+    //     });
+
+    //     // Unzoom and close the panel, zoom the active pane.
+    //     panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
+    //     workspace.update(cx, |workspace, cx| {
+    //         workspace.toggle_dock(DockPosition::Right, cx)
+    //     });
+    //     pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
+
+    //     // Opening a dock unzooms the pane.
+    //     workspace.update(cx, |workspace, cx| {
+    //         workspace.toggle_dock(DockPosition::Right, cx)
+    //     });
+    //     workspace.update(cx, |workspace, cx| {
+    //         let pane = pane.read(cx);
+    //         assert!(!pane.is_zoomed());
+    //         assert!(!pane.focus_handle(cx).is_focused(cx));
+    //         assert!(workspace.right_dock().read(cx).is_open());
+    //         assert!(workspace.zoomed.is_none());
+    //     });
+    // }
 
-    //         // Opening a dock unzooms the pane.
-    //         workspace.update(cx, |workspace, cx| {
-    //             workspace.toggle_dock(DockPosition::Right, cx)
-    //         });
-    //         workspace.update(cx, |workspace, cx| {
-    //             let pane = pane.read(cx);
-    //             assert!(!pane.is_zoomed());
-    //             assert!(!pane.has_focus());
-    //             assert!(workspace.right_dock().read(cx).is_open());
-    //             assert!(workspace.zoomed.is_none());
+    // #[gpui::test]
+    // async fn test_panels(cx: &mut gpui::TestAppContext) {
+    //     init_test(cx);
+    //     let fs = FakeFs::new(cx.executor());
+
+    //     let project = Project::test(fs, [], cx).await;
+    //     let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
+
+    //     let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
+    //         // Add panel_1 on the left, panel_2 on the right.
+    //         let panel_1 = cx.build_view(|cx| TestPanel::new(DockPosition::Left, cx));
+    //         workspace.add_panel(panel_1.clone(), cx);
+    //         workspace
+    //             .left_dock()
+    //             .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
+    //         let panel_2 = cx.build_view(|cx| TestPanel::new(DockPosition::Right, cx));
+    //         workspace.add_panel(panel_2.clone(), cx);
+    //         workspace
+    //             .right_dock()
+    //             .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
+
+    //         let left_dock = workspace.left_dock();
+    //         assert_eq!(
+    //             left_dock.read(cx).visible_panel().unwrap().panel_id(),
+    //             panel_1.panel_id()
+    //         );
+    //         assert_eq!(
+    //             left_dock.read(cx).active_panel_size(cx).unwrap(),
+    //             panel_1.size(cx)
+    //         );
+
+    //         left_dock.update(cx, |left_dock, cx| {
+    //             left_dock.resize_active_panel(Some(1337.), cx)
     //         });
-    //     }
-
-    //     #[gpui::test]
-    //     async fn test_panels(cx: &mut gpui::TestAppContext) {
-    //         init_test(cx);
-    //         let fs = FakeFs::new(cx.executor());
-
-    //         let project = Project::test(fs, [], cx).await;
-    //         let window = cx.add_window(|cx| Workspace::test_new(project, cx));
-    //         let workspace = window.root(cx);
-
-    //         let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
-    //             // Add panel_1 on the left, panel_2 on the right.
-    //             let panel_1 = cx.build_view(|_| TestPanel::new(DockPosition::Left));
-    //             workspace.add_panel(panel_1.clone(), cx);
+    //         assert_eq!(
     //             workspace
-    //                 .left_dock()
-    //                 .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
-    //             let panel_2 = cx.build_view(|_| TestPanel::new(DockPosition::Right));
-    //             workspace.add_panel(panel_2.clone(), cx);
+    //                 .right_dock()
+    //                 .read(cx)
+    //                 .visible_panel()
+    //                 .unwrap()
+    //                 .panel_id(),
+    //             panel_2.panel_id(),
+    //         );
+
+    //         (panel_1, panel_2)
+    //     });
+
+    //     // Move panel_1 to the right
+    //     panel_1.update(cx, |panel_1, cx| {
+    //         panel_1.set_position(DockPosition::Right, cx)
+    //     });
+
+    //     workspace.update(cx, |workspace, cx| {
+    //         // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
+    //         // Since it was the only panel on the left, the left dock should now be closed.
+    //         assert!(!workspace.left_dock().read(cx).is_open());
+    //         assert!(workspace.left_dock().read(cx).visible_panel().is_none());
+    //         let right_dock = workspace.right_dock();
+    //         assert_eq!(
+    //             right_dock.read(cx).visible_panel().unwrap().panel_id(),
+    //             panel_1.panel_id()
+    //         );
+    //         assert_eq!(right_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
+
+    //         // Now we move panel_2Β to the left
+    //         panel_2.set_position(DockPosition::Left, cx);
+    //     });
+
+    //     workspace.update(cx, |workspace, cx| {
+    //         // Since panel_2 was not visible on the right, we don't open the left dock.
+    //         assert!(!workspace.left_dock().read(cx).is_open());
+    //         // And the right dock is unaffected in it's displaying of panel_1
+    //         assert!(workspace.right_dock().read(cx).is_open());
+    //         assert_eq!(
     //             workspace
     //                 .right_dock()
-    //                 .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
-
-    //             let left_dock = workspace.left_dock();
-    //             assert_eq!(
-    //                 left_dock.read(cx).visible_panel().unwrap().id(),
-    //                 panel_1.id()
-    //             );
-    //             assert_eq!(
-    //                 left_dock.read(cx).active_panel_size(cx).unwrap(),
-    //                 panel_1.size(cx)
-    //             );
-
-    //             left_dock.update(cx, |left_dock, cx| {
-    //                 left_dock.resize_active_panel(Some(1337.), cx)
-    //             });
-    //             assert_eq!(
-    //                 workspace
-    //                     .right_dock()
-    //                     .read(cx)
-    //                     .visible_panel()
-    //                     .unwrap()
-    //                     .id(),
-    //                 panel_2.id()
-    //             );
-
-    //             (panel_1, panel_2)
-    //         });
-
-    //         // Move panel_1 to the right
-    //         panel_1.update(cx, |panel_1, cx| {
-    //             panel_1.set_position(DockPosition::Right, cx)
-    //         });
-
-    //         workspace.update(cx, |workspace, cx| {
-    //             // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
-    //             // Since it was the only panel on the left, the left dock should now be closed.
-    //             assert!(!workspace.left_dock().read(cx).is_open());
-    //             assert!(workspace.left_dock().read(cx).visible_panel().is_none());
-    //             let right_dock = workspace.right_dock();
-    //             assert_eq!(
-    //                 right_dock.read(cx).visible_panel().unwrap().id(),
-    //                 panel_1.id()
-    //             );
-    //             assert_eq!(right_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
-
-    //             // Now we move panel_2Β to the left
-    //             panel_2.set_position(DockPosition::Left, cx);
-    //         });
-
-    //         workspace.update(cx, |workspace, cx| {
-    //             // Since panel_2 was not visible on the right, we don't open the left dock.
-    //             assert!(!workspace.left_dock().read(cx).is_open());
-    //             // And the right dock is unaffected in it's displaying of panel_1
-    //             assert!(workspace.right_dock().read(cx).is_open());
-    //             assert_eq!(
-    //                 workspace
-    //                     .right_dock()
-    //                     .read(cx)
-    //                     .visible_panel()
-    //                     .unwrap()
-    //                     .id(),
-    //                 panel_1.id()
-    //             );
-    //         });
-
-    //         // Move panel_1 back to the left
-    //         panel_1.update(cx, |panel_1, cx| {
-    //             panel_1.set_position(DockPosition::Left, cx)
-    //         });
-
-    //         workspace.update(cx, |workspace, cx| {
-    //             // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
-    //             let left_dock = workspace.left_dock();
-    //             assert!(left_dock.read(cx).is_open());
-    //             assert_eq!(
-    //                 left_dock.read(cx).visible_panel().unwrap().id(),
-    //                 panel_1.id()
-    //             );
-    //             assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
-    //             // And right the dock should be closed as it no longer has any panels.
-    //             assert!(!workspace.right_dock().read(cx).is_open());
-
-    //             // Now we move panel_1 to the bottom
-    //             panel_1.set_position(DockPosition::Bottom, cx);
-    //         });
-
-    //         workspace.update(cx, |workspace, cx| {
-    //             // Since panel_1 was visible on the left, we close the left dock.
-    //             assert!(!workspace.left_dock().read(cx).is_open());
-    //             // The bottom dock is sized based on the panel's default size,
-    //             // since the panel orientation changed from vertical to horizontal.
-    //             let bottom_dock = workspace.bottom_dock();
-    //             assert_eq!(
-    //                 bottom_dock.read(cx).active_panel_size(cx).unwrap(),
-    //                 panel_1.size(cx),
-    //             );
-    //             // Close bottom dock and move panel_1 back to the left.
-    //             bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
-    //             panel_1.set_position(DockPosition::Left, cx);
-    //         });
-
-    //         // Emit activated event on panel 1
-    //         panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Activated));
-
-    //         // Now the left dock is open and panel_1 is active and focused.
-    //         workspace.update(cx, |workspace, cx| {
-    //             let left_dock = workspace.left_dock();
-    //             assert!(left_dock.read(cx).is_open());
-    //             assert_eq!(
-    //                 left_dock.read(cx).visible_panel().unwrap().id(),
-    //                 panel_1.id()
-    //             );
-    //             assert!(panel_1.is_focused(cx));
-    //         });
-
-    //         // Emit closed event on panel 2, which is not active
-    //         panel_2.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
-
-    //         // Wo don't close the left dock, because panel_2 wasn't the active panel
-    //         workspace.update(cx, |workspace, cx| {
-    //             let left_dock = workspace.left_dock();
-    //             assert!(left_dock.read(cx).is_open());
-    //             assert_eq!(
-    //                 left_dock.read(cx).visible_panel().unwrap().id(),
-    //                 panel_1.id()
-    //             );
-    //         });
-
-    //         // Emitting a ZoomIn event shows the panel as zoomed.
-    //         panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomIn));
-    //         workspace.update(cx, |workspace, _| {
-    //             assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
-    //             assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
-    //         });
-
-    //         // Move panel to another dock while it is zoomed
-    //         panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
-    //         workspace.update(cx, |workspace, _| {
-    //             assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
-    //             assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
-    //         });
-
-    //         // If focus is transferred to another view that's not a panel or another pane, we still show
-    //         // the panel as zoomed.
-    //         let focus_receiver = cx.build_view(|_| EmptyView);
-    //         focus_receiver.update(cx, |_, cx| cx.focus_self());
-    //         workspace.update(cx, |workspace, _| {
-    //             assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
-    //             assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
-    //         });
-
-    //         // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
-    //         workspace.update(cx, |_, cx| cx.focus_self());
-    //         workspace.update(cx, |workspace, _| {
-    //             assert_eq!(workspace.zoomed, None);
-    //             assert_eq!(workspace.zoomed_position, None);
-    //         });
-
-    //         // If focus is transferred again to another view that's not a panel or a pane, we won't
-    //         // show the panel as zoomed because it wasn't zoomed before.
-    //         focus_receiver.update(cx, |_, cx| cx.focus_self());
-    //         workspace.update(cx, |workspace, _| {
-    //             assert_eq!(workspace.zoomed, None);
-    //             assert_eq!(workspace.zoomed_position, None);
-    //         });
-
-    //         // When focus is transferred back to the panel, it is zoomed again.
-    //         panel_1.update(cx, |_, cx| cx.focus_self());
-    //         workspace.update(cx, |workspace, _| {
-    //             assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
-    //             assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
-    //         });
-
-    //         // Emitting a ZoomOut event unzooms the panel.
-    //         panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomOut));
-    //         workspace.update(cx, |workspace, _| {
-    //             assert_eq!(workspace.zoomed, None);
-    //             assert_eq!(workspace.zoomed_position, None);
-    //         });
-
-    //         // Emit closed event on panel 1, which is active
-    //         panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed));
-
-    //         // Now the left dock is closed, because panel_1 was the active panel
-    //         workspace.update(cx, |workspace, cx| {
-    //             let right_dock = workspace.right_dock();
-    //             assert!(!right_dock.read(cx).is_open());
-    //         });
-    //     }
+    //                 .read(cx)
+    //                 .visible_panel()
+    //                 .unwrap()
+    //                 .panel_id(),
+    //             panel_1.panel_id(),
+    //         );
+    //     });
+
+    //     // Move panel_1 back to the left
+    //     panel_1.update(cx, |panel_1, cx| {
+    //         panel_1.set_position(DockPosition::Left, cx)
+    //     });
+
+    //     workspace.update(cx, |workspace, cx| {
+    //         // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
+    //         let left_dock = workspace.left_dock();
+    //         assert!(left_dock.read(cx).is_open());
+    //         assert_eq!(
+    //             left_dock.read(cx).visible_panel().unwrap().panel_id(),
+    //             panel_1.panel_id()
+    //         );
+    //         assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
+    //         // And right the dock should be closed as it no longer has any panels.
+    //         assert!(!workspace.right_dock().read(cx).is_open());
+
+    //         // Now we move panel_1 to the bottom
+    //         panel_1.set_position(DockPosition::Bottom, cx);
+    //     });
+
+    //     workspace.update(cx, |workspace, cx| {
+    //         // Since panel_1 was visible on the left, we close the left dock.
+    //         assert!(!workspace.left_dock().read(cx).is_open());
+    //         // The bottom dock is sized based on the panel's default size,
+    //         // since the panel orientation changed from vertical to horizontal.
+    //         let bottom_dock = workspace.bottom_dock();
+    //         assert_eq!(
+    //             bottom_dock.read(cx).active_panel_size(cx).unwrap(),
+    //             panel_1.size(cx),
+    //         );
+    //         // Close bottom dock and move panel_1 back to the left.
+    //         bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
+    //         panel_1.set_position(DockPosition::Left, cx);
+    //     });
+
+    //     // Emit activated event on panel 1
+    //     panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
+
+    //     // Now the left dock is open and panel_1 is active and focused.
+    //     workspace.update(cx, |workspace, cx| {
+    //         let left_dock = workspace.left_dock();
+    //         assert!(left_dock.read(cx).is_open());
+    //         assert_eq!(
+    //             left_dock.read(cx).visible_panel().unwrap().panel_id(),
+    //             panel_1.panel_id(),
+    //         );
+    //         assert!(panel_1.focus_handle(cx).is_focused(cx));
+    //     });
+
+    //     // Emit closed event on panel 2, which is not active
+    //     panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
+
+    //     // Wo don't close the left dock, because panel_2 wasn't the active panel
+    //     workspace.update(cx, |workspace, cx| {
+    //         let left_dock = workspace.left_dock();
+    //         assert!(left_dock.read(cx).is_open());
+    //         assert_eq!(
+    //             left_dock.read(cx).visible_panel().unwrap().panel_id(),
+    //             panel_1.panel_id(),
+    //         );
+    //     });
+
+    //     // Emitting a ZoomIn event shows the panel as zoomed.
+    //     panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
+    //     workspace.update(cx, |workspace, _| {
+    //         assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
+    //         assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
+    //     });
+
+    //     // Move panel to another dock while it is zoomed
+    //     panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
+    //     workspace.update(cx, |workspace, _| {
+    //         assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
+
+    //         assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
+    //     });
+
+    //     // If focus is transferred to another view that's not a panel or another pane, we still show
+    //     // the panel as zoomed.
+    //     let other_focus_handle = cx.update(|cx| cx.focus_handle());
+    //     cx.update(|cx| cx.focus(&other_focus_handle));
+    //     workspace.update(cx, |workspace, _| {
+    //         assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
+    //         assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
+    //     });
+
+    //     // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
+    //     workspace.update(cx, |_, cx| cx.focus_self());
+    //     workspace.update(cx, |workspace, _| {
+    //         assert_eq!(workspace.zoomed, None);
+    //         assert_eq!(workspace.zoomed_position, None);
+    //     });
+
+    //     // If focus is transferred again to another view that's not a panel or a pane, we won't
+    //     // show the panel as zoomed because it wasn't zoomed before.
+    //     cx.update(|cx| cx.focus(&other_focus_handle));
+    //     workspace.update(cx, |workspace, _| {
+    //         assert_eq!(workspace.zoomed, None);
+    //         assert_eq!(workspace.zoomed_position, None);
+    //     });
+
+    //     // When focus is transferred back to the panel, it is zoomed again.
+    //     panel_1.update(cx, |_, cx| cx.focus_self());
+    //     workspace.update(cx, |workspace, _| {
+    //         assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
+    //         assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
+    //     });
+
+    //     // Emitting a ZoomOut event unzooms the panel.
+    //     panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
+    //     workspace.update(cx, |workspace, _| {
+    //         assert_eq!(workspace.zoomed, None);
+    //         assert_eq!(workspace.zoomed_position, None);
+    //     });
+
+    //     // Emit closed event on panel 1, which is active
+    //     panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
+
+    //     // Now the left dock is closed, because panel_1 was the active panel
+    //     workspace.update(cx, |workspace, cx| {
+    //         let right_dock = workspace.right_dock();
+    //         assert!(!right_dock.read(cx).is_open());
+    //     });
+    // }
 
     pub fn init_test(cx: &mut TestAppContext) {
         cx.update(|cx| {

crates/zed2/src/zed2.rs πŸ”—

@@ -426,7 +426,7 @@ fn initialize_pane(workspace: &mut Workspace, pane: &View<Pane>, cx: &mut ViewCo
                 cx.build_view(|_| QuickActionBar::new(buffer_search_bar, workspace));
             toolbar.add_item(quick_action_bar, cx);
             let diagnostic_editor_controls = cx.build_view(|_| diagnostics::ToolbarControls::new());
-            //     toolbar.add_item(diagnostic_editor_controls, cx);
+            toolbar.add_item(diagnostic_editor_controls, cx);
             let project_search_bar = cx.build_view(|_| ProjectSearchBar::new());
             toolbar.add_item(project_search_bar, cx);
             //     let lsp_log_item =

debug.plist πŸ”—

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+    <dict>
+        <key>com.apple.security.get-task-allow</key>
+        <true/>
+    </dict>
+</plist>

docker-compose.yml πŸ”—

@@ -0,0 +1,16 @@
+version: "3.7"
+
+services:
+  postgres:
+    image: postgres:15
+    container_name: zed_postgres
+    ports:
+      - 5432:5432
+    environment:
+      POSTGRES_HOST_AUTH_METHOD: trust
+    volumes:
+      - postgres_data:/var/lib/postgresql/data
+      - ./docker-compose.sql:/docker-entrypoint-initdb.d/init.sql
+
+volumes:
+  postgres_data:

script/storybook πŸ”—

@@ -0,0 +1,7 @@
+#!/bin/bash
+
+if [ -z "$1" ]; then
+    cargo run -p storybook2
+else
+    cargo run -p storybook2 -- "components/$1"
+fi