Merge branch 'main' into terminal-hyperlinks

Mikayla Maki created

Change summary

Cargo.lock                                                              | 103 
assets/icons/zed_22.svg                                                 |   4 
assets/keymaps/default.json                                             |  24 
assets/settings/default.json                                            |  10 
crates/activity_indicator/src/activity_indicator.rs                     |   2 
crates/auto_update/src/update_notification.rs                           |   4 
crates/capture/Cargo.toml                                               |  32 
crates/capture/build.rs                                                 |   7 
crates/capture/src/live_kit_token.rs                                    |  71 
crates/capture/src/main.rs                                              | 143 
crates/chat_panel/src/chat_panel.rs                                     |   2 
crates/collab/src/integration_tests.rs                                  |  14 
crates/command_palette/src/command_palette.rs                           |   3 
crates/contacts_panel/src/contacts_panel.rs                             |  75 
crates/contacts_panel/src/notifications.rs                              |   4 
crates/contacts_status_item/Cargo.toml                                  |  32 
crates/contacts_status_item/src/contacts_popover.rs                     |  94 
crates/contacts_status_item/src/contacts_status_item.rs                 |  94 
crates/context_menu/src/context_menu.rs                                 |  24 
crates/diagnostics/src/diagnostics.rs                                   |   5 
crates/diagnostics/src/items.rs                                         |   4 
crates/drag_and_drop/src/drag_and_drop.rs                               |  49 
crates/editor/src/editor.rs                                             |  37 
crates/editor/src/element.rs                                            | 582 
crates/editor/src/hover_popover.rs                                      |  19 
crates/editor/src/mouse_context_menu.rs                                 |   6 
crates/editor/src/test.rs                                               |   5 
crates/file_finder/src/file_finder.rs                                   |  18 
crates/gpui/Cargo.toml                                                  |   2 
crates/gpui/src/app.rs                                                  | 165 
crates/gpui/src/elements.rs                                             |  27 
crates/gpui/src/elements/container.rs                                   |  18 
crates/gpui/src/elements/event_handler.rs                               | 177 
crates/gpui/src/elements/flex.rs                                        | 122 
crates/gpui/src/elements/list.rs                                        |  43 
crates/gpui/src/elements/mouse_event_handler.rs                         |  34 
crates/gpui/src/elements/overlay.rs                                     | 147 
crates/gpui/src/elements/resizable.rs                                   | 225 
crates/gpui/src/elements/text.rs                                        |   2 
crates/gpui/src/elements/tooltip.rs                                     |  55 
crates/gpui/src/elements/uniform_list.rs                                |  47 
crates/gpui/src/platform.rs                                             |  74 
crates/gpui/src/platform/mac.rs                                         |   3 
crates/gpui/src/platform/mac/appearance.rs                              |  37 
crates/gpui/src/platform/mac/platform.rs                                |  46 
crates/gpui/src/platform/mac/renderer.rs                                | 227 
crates/gpui/src/platform/mac/shaders/shaders.h                          | 185 
crates/gpui/src/platform/mac/shaders/shaders.metal                      |  48 
crates/gpui/src/platform/mac/status_item.rs                             | 367 
crates/gpui/src/platform/mac/window.rs                                  | 397 
crates/gpui/src/platform/test.rs                                        |  67 
crates/gpui/src/presenter.rs                                            | 170 
crates/gpui/src/scene.rs                                                |  54 
crates/gpui/src/scene/mouse_region.rs                                   | 109 
crates/gpui/src/scene/mouse_region_event.rs                             |   2 
crates/gpui/src/views/select.rs                                         |  30 
crates/live_kit/Cargo.toml                                              |  22 
crates/live_kit/LiveKitBridge/.gitignore                                |   9 
crates/live_kit/LiveKitBridge/Package.resolved                          |  52 
crates/live_kit/LiveKitBridge/Package.swift                             |  27 
crates/live_kit/LiveKitBridge/README.md                                 |   3 
crates/live_kit/LiveKitBridge/Sources/LiveKitBridge/LiveKitBridge.swift | 105 
crates/live_kit/build.rs                                                | 148 
crates/live_kit/src/live_kit.rs                                         | 276 
crates/media/Cargo.toml                                                 |  20 
crates/media/build.rs                                                   |  39 
crates/media/src/bindings.h                                             |   5 
crates/media/src/bindings.rs                                            |   8 
crates/media/src/media.rs                                               | 534 
crates/picker/src/picker.rs                                             |   2 
crates/project_panel/src/project_panel.rs                               |  16 
crates/search/src/buffer_search.rs                                      |   4 
crates/search/src/project_search.rs                                     |   6 
crates/settings/src/settings.rs                                         |  16 
crates/terminal/src/mappings/mouse.rs                                   |   3 
crates/terminal/src/modal.rs                                            |  78 
crates/terminal/src/terminal.rs                                         |  23 
crates/terminal/src/terminal_element.rs                                 |  79 
crates/terminal/src/terminal_view.rs                                    |   7 
crates/terminal/src/tests/terminal_test_context.rs                      |   4 
crates/theme/src/theme.rs                                               |  29 
crates/vim/src/vim_test_context.rs                                      |   3 
crates/workspace/src/dock.rs                                            | 718 
crates/workspace/src/pane.rs                                            | 349 
crates/workspace/src/pane_group.rs                                      |   4 
crates/workspace/src/programs.rs                                        |  77 
crates/workspace/src/sidebar.rs                                         | 109 
crates/workspace/src/toolbar.rs                                         |   2 
crates/workspace/src/waiting_room.rs                                    | 144 
crates/workspace/src/workspace.rs                                       | 260 
crates/zed/Cargo.toml                                                   |   3 
crates/zed/src/feedback.rs                                              |   2 
crates/zed/src/languages/json.rs                                        |  88 
crates/zed/src/main.rs                                                  |  25 
crates/zed/src/zed.rs                                                   |  47 
script/bundle                                                           |   1 
styles/package-lock.json                                                |   1 
styles/src/styleTree/app.ts                                             |   2 
styles/src/styleTree/contactsPopover.ts                                 |   8 
styles/src/styleTree/search.ts                                          |   5 
styles/src/styleTree/tabBar.ts                                          |  20 
styles/src/styleTree/workspace.ts                                       |  29 
102 files changed, 5,843 insertions(+), 1,916 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -663,9 +663,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
 
 [[package]]
 name = "bytes"
-version = "1.1.0"
+version = "1.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
+checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db"
 
 [[package]]
 name = "bzip2-sys"
@@ -750,6 +750,34 @@ dependencies = [
  "winx",
 ]
 
+[[package]]
+name = "capture"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "bindgen",
+ "block",
+ "byteorder",
+ "bytes",
+ "cocoa",
+ "core-foundation",
+ "core-graphics",
+ "foreign-types",
+ "futures",
+ "gpui",
+ "hmac 0.12.1",
+ "jwt",
+ "live_kit",
+ "log",
+ "media",
+ "objc",
+ "parking_lot 0.11.2",
+ "postage",
+ "serde",
+ "sha2 0.10.2",
+ "simplelog",
+]
+
 [[package]]
 name = "castaway"
 version = "0.1.2"
@@ -1098,6 +1126,30 @@ dependencies = [
  "workspace",
 ]
 
+[[package]]
+name = "contacts_status_item"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "client",
+ "collections",
+ "editor",
+ "futures",
+ "fuzzy",
+ "gpui",
+ "language",
+ "log",
+ "menu",
+ "picker",
+ "postage",
+ "project",
+ "serde",
+ "settings",
+ "theme",
+ "util",
+ "workspace",
+]
+
 [[package]]
 name = "context_menu"
 version = "0.1.0"
@@ -2213,6 +2265,7 @@ dependencies = [
  "image",
  "lazy_static",
  "log",
+ "media",
  "metal",
  "num_cpus",
  "objc",
@@ -2722,6 +2775,21 @@ version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "41ee439ee368ba4a77ac70d04f14015415af8600d6c894dc1f11bd79758c57d5"
 
+[[package]]
+name = "jwt"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6204285f77fe7d9784db3fdc449ecce1a0114927a51d5a41c4c7a292011c015f"
+dependencies = [
+ "base64",
+ "crypto-common",
+ "digest 0.10.3",
+ "hmac 0.12.1",
+ "serde",
+ "serde_json",
+ "sha2 0.10.2",
+]
+
 [[package]]
 name = "kernel32-sys"
 version = "0.2.2"
@@ -2892,6 +2960,20 @@ dependencies = [
  "rand_chacha 0.3.1",
 ]
 
+[[package]]
+name = "live_kit"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "core-foundation",
+ "core-graphics",
+ "futures",
+ "media",
+ "parking_lot 0.11.2",
+ "serde",
+ "serde_json",
+]
+
 [[package]]
 name = "lock_api"
 version = "0.4.7"
@@ -3008,6 +3090,20 @@ dependencies = [
  "digest 0.10.3",
 ]
 
+[[package]]
+name = "media"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "bindgen",
+ "block",
+ "bytes",
+ "core-foundation",
+ "foreign-types",
+ "metal",
+ "objc",
+]
+
 [[package]]
 name = "memchr"
 version = "2.5.0"
@@ -7033,7 +7129,7 @@ dependencies = [
 
 [[package]]
 name = "zed"
-version = "0.53.1"
+version = "0.54.1"
 dependencies = [
  "activity_indicator",
  "anyhow",
@@ -7052,6 +7148,7 @@ dependencies = [
  "collections",
  "command_palette",
  "contacts_panel",
+ "contacts_status_item",
  "context_menu",
  "ctor",
  "diagnostics",

assets/icons/zed_22.svg 🔗

@@ -0,0 +1,4 @@
+<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M5 11C5 14.3137 7.68629 17 11 17C14.3137 17 17 14.3137 17 11C17 7.68629 14.3137 5 11 5C7.68629 5 5 7.68629 5 11ZM11 3C6.58172 3 3 6.58172 3 11C3 15.4183 6.58172 19 11 19C15.4183 19 19 15.4183 19 11C19 6.58172 15.4183 3 11 3Z" fill="white"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M8.09092 8.09088H14.6364L10.5511 12.4545H12.4546L13.9091 13.9091H7.36365L11.7273 9.54543H9.54547L8.09092 8.09088Z" fill="white"/>
+</svg>

assets/keymaps/default.json 🔗

@@ -309,8 +309,7 @@
             "cmd-shift-p": "command_palette::Toggle",
             "cmd-shift-m": "diagnostics::Deploy",
             "cmd-shift-e": "project_panel::ToggleFocus",
-            "cmd-alt-s": "workspace::SaveAll",
-            "shift-escape": "terminal::DeployModal"
+            "cmd-alt-s": "workspace::SaveAll"
         }
     },
     // Bindings from Sublime Text
@@ -394,10 +393,24 @@
     {
         "context": "Workspace",
         "bindings": {
+            "shift-escape": "dock::FocusDock",
             "cmd-shift-c": "contacts_panel::ToggleFocus",
             "cmd-shift-b": "workspace::ToggleRightSidebar"
         }
     },
+    {
+        "bindings": {
+            "cmd-shift-k cmd-shift-right": "dock::AnchorDockRight",
+            "cmd-shift-k cmd-shift-down": "dock::AnchorDockBottom",
+            "cmd-shift-k cmd-shift-up": "dock::ExpandDock"
+        }
+    },
+    {
+        "context": "Dock",
+        "bindings": {
+            "shift-escape": "dock::HideDock"
+        }
+    },
     {
         "context": "ProjectPanel",
         "bindings": {
@@ -426,12 +439,5 @@
             "cmd-v": "terminal::Paste",
             "cmd-k": "terminal::Clear"
         }
-    },
-    {
-        "context": "ModalTerminal",
-        "bindings": {
-            "ctrl-cmd-space": "terminal::ShowCharacterPalette",
-            "shift-escape": "terminal::DeployModal"
-        }
     }
 ]

assets/settings/default.json 🔗

@@ -32,6 +32,16 @@
     // 4. Save when idle for a certain amount of time:
     //     "autosave": { "after_delay": {"milliseconds": 500} },
     "autosave": "off",
+    // Where to place the dock by default. This setting can take three
+    // values:
+    //
+    // 1. Position the dock attached to the bottom of the workspace
+    //     "default_dock_anchor": "bottom"
+    // 2. Position the dock to the right of the workspace like a side panel
+    //     "default_dock_anchor": "right"
+    // 3. Position the dock full screen over the entire workspace"
+    //     "default_dock_anchor": "expanded"
+    "default_dock_anchor": "right",
     // How to auto-format modified buffers when saving them. This
     // setting can take three values:
     //

crates/activity_indicator/src/activity_indicator.rs 🔗

@@ -278,7 +278,7 @@ impl View for ActivityIndicator {
     fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
         let (icon, message, action) = self.content_to_render(cx);
 
-        let mut element = MouseEventHandler::new::<Self, _, _>(0, cx, |state, cx| {
+        let mut element = MouseEventHandler::<Self>::new(0, cx, |state, cx| {
             let theme = &cx
                 .global::<Settings>()
                 .theme

crates/auto_update/src/update_notification.rs 🔗

@@ -29,7 +29,7 @@ impl View for UpdateNotification {
         let theme = cx.global::<Settings>().theme.clone();
         let theme = &theme.update_notification;
 
-        MouseEventHandler::new::<ViewReleaseNotes, _, _>(0, cx, |state, cx| {
+        MouseEventHandler::<ViewReleaseNotes>::new(0, cx, |state, cx| {
             Flex::column()
                 .with_child(
                     Flex::row()
@@ -47,7 +47,7 @@ impl View for UpdateNotification {
                             .boxed(),
                         )
                         .with_child(
-                            MouseEventHandler::new::<Cancel, _, _>(0, cx, |state, _| {
+                            MouseEventHandler::<Cancel>::new(0, cx, |state, _| {
                                 let style = theme.dismiss_button.style_for(state, false);
                                 Svg::new("icons/x_mark_thin_8.svg")
                                     .with_color(style.color)

crates/capture/Cargo.toml 🔗

@@ -0,0 +1,32 @@
+[package]
+name = "capture"
+version = "0.1.0"
+edition = "2021"
+description = "An example of screen capture"
+
+[dependencies]
+gpui = { path = "../gpui" }
+live_kit = { path = "../live_kit" }
+media = { path = "../media" }
+
+anyhow = "1.0.38"
+block = "0.1"
+bytes = "1.2"
+byteorder = "1.4"
+cocoa = "0.24"
+core-foundation = "0.9.3"
+core-graphics = "0.22.3"
+foreign-types = "0.3"
+futures = "0.3"
+hmac = "0.12"
+jwt = "0.16"
+log = { version = "0.4.16", features = ["kv_unstable_serde"] }
+objc = "0.2"
+parking_lot = "0.11.1"
+postage = { version = "0.4.1", features = ["futures-traits"] }
+serde = { version = "1.0", features = ["derive", "rc"] }
+sha2 = "0.10"
+simplelog = "0.9"
+
+[build-dependencies]
+bindgen = "0.59.2"

crates/capture/build.rs 🔗

@@ -0,0 +1,7 @@
+fn main() {
+    // Find WebRTC.framework as a sibling of the executable when running outside of an application bundle
+    println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path");
+
+    // Register exported Objective-C selectors, protocols, etc
+    println!("cargo:rustc-link-arg=-Wl,-ObjC");
+}

crates/capture/src/live_kit_token.rs 🔗

@@ -0,0 +1,71 @@
+use anyhow::Result;
+use hmac::{Hmac, Mac};
+use jwt::SignWithKey;
+use serde::Serialize;
+use sha2::Sha256;
+use std::{
+    ops::Add,
+    time::{Duration, SystemTime, UNIX_EPOCH},
+};
+
+static DEFAULT_TTL: Duration = Duration::from_secs(6 * 60 * 60); // 6 hours
+
+#[derive(Default, Serialize)]
+#[serde(rename_all = "camelCase")]
+struct ClaimGrants<'a> {
+    iss: &'a str,
+    sub: &'a str,
+    iat: u64,
+    exp: u64,
+    nbf: u64,
+    jwtid: &'a str,
+    video: VideoGrant<'a>,
+}
+
+#[derive(Default, Serialize)]
+#[serde(rename_all = "camelCase")]
+struct VideoGrant<'a> {
+    room_create: Option<bool>,
+    room_join: Option<bool>,
+    room_list: Option<bool>,
+    room_record: Option<bool>,
+    room_admin: Option<bool>,
+    room: Option<&'a str>,
+    can_publish: Option<bool>,
+    can_subscribe: Option<bool>,
+    can_publish_data: Option<bool>,
+    hidden: Option<bool>,
+    recorder: Option<bool>,
+}
+
+pub fn create_token(
+    api_key: &str,
+    secret_key: &str,
+    room_name: &str,
+    participant_name: &str,
+) -> Result<String> {
+    let secret_key: Hmac<Sha256> = Hmac::new_from_slice(secret_key.as_bytes())?;
+
+    let now = SystemTime::now();
+
+    let claims = ClaimGrants {
+        iss: api_key,
+        sub: participant_name,
+        iat: now.duration_since(UNIX_EPOCH).unwrap().as_secs(),
+        exp: now
+            .add(DEFAULT_TTL)
+            .duration_since(UNIX_EPOCH)
+            .unwrap()
+            .as_secs(),
+        nbf: 0,
+        jwtid: participant_name,
+        video: VideoGrant {
+            room: Some(room_name),
+            room_join: Some(true),
+            can_publish: Some(true),
+            can_subscribe: Some(true),
+            ..Default::default()
+        },
+    };
+    Ok(claims.sign_with_key(&secret_key)?)
+}

crates/capture/src/main.rs 🔗

@@ -0,0 +1,143 @@
+mod live_kit_token;
+
+use futures::StreamExt;
+use gpui::{
+    actions,
+    elements::{Canvas, *},
+    keymap::Binding,
+    platform::current::Surface,
+    Menu, MenuItem, ViewContext,
+};
+use live_kit::{LocalVideoTrack, Room};
+use log::LevelFilter;
+use media::core_video::CVImageBuffer;
+use postage::watch;
+use simplelog::SimpleLogger;
+use std::sync::Arc;
+
+actions!(capture, [Quit]);
+
+fn main() {
+    SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
+
+    gpui::App::new(()).unwrap().run(|cx| {
+        cx.platform().activate(true);
+        cx.add_global_action(quit);
+
+        cx.add_bindings([Binding::new("cmd-q", Quit, None)]);
+        cx.set_menus(vec![Menu {
+            name: "Zed",
+            items: vec![MenuItem::Action {
+                name: "Quit",
+                action: Box::new(Quit),
+            }],
+        }]);
+
+        let live_kit_url = std::env::var("LIVE_KIT_URL").unwrap();
+        let live_kit_key = std::env::var("LIVE_KIT_KEY").unwrap();
+        let live_kit_secret = std::env::var("LIVE_KIT_SECRET").unwrap();
+
+        cx.spawn(|mut cx| async move {
+            let user1_token = live_kit_token::create_token(
+                &live_kit_key,
+                &live_kit_secret,
+                "test-room",
+                "test-participant-1",
+            )
+            .unwrap();
+            let room1 = Room::new();
+            room1.connect(&live_kit_url, &user1_token).await.unwrap();
+
+            let user2_token = live_kit_token::create_token(
+                &live_kit_key,
+                &live_kit_secret,
+                "test-room",
+                "test-participant-2",
+            )
+            .unwrap();
+            let room2 = Room::new();
+            room2.connect(&live_kit_url, &user2_token).await.unwrap();
+            cx.add_window(Default::default(), |cx| ScreenCaptureView::new(room2, cx));
+
+            let windows = live_kit::list_windows();
+            let window = windows
+                .iter()
+                .find(|w| w.owner_name.as_deref() == Some("Safari"))
+                .unwrap();
+            let track = LocalVideoTrack::screen_share_for_window(window.id);
+            room1.publish_video_track(&track).await.unwrap();
+        })
+        .detach();
+    });
+}
+
+struct ScreenCaptureView {
+    image_buffer: Option<CVImageBuffer>,
+    _room: Arc<Room>,
+}
+
+impl gpui::Entity for ScreenCaptureView {
+    type Event = ();
+}
+
+impl ScreenCaptureView {
+    pub fn new(room: Arc<Room>, cx: &mut ViewContext<Self>) -> Self {
+        let mut remote_video_tracks = room.remote_video_tracks();
+        cx.spawn_weak(|this, mut cx| async move {
+            if let Some(video_track) = remote_video_tracks.next().await {
+                let (mut frames_tx, mut frames_rx) = watch::channel_with(None);
+                video_track.add_renderer(move |frame| *frames_tx.borrow_mut() = Some(frame));
+
+                while let Some(frame) = frames_rx.next().await {
+                    if let Some(this) = this.upgrade(&cx) {
+                        this.update(&mut cx, |this, cx| {
+                            this.image_buffer = frame;
+                            cx.notify();
+                        });
+                    } else {
+                        break;
+                    }
+                }
+            }
+        })
+        .detach();
+
+        Self {
+            image_buffer: None,
+            _room: room,
+        }
+    }
+}
+
+impl gpui::View for ScreenCaptureView {
+    fn ui_name() -> &'static str {
+        "View"
+    }
+
+    fn render(&mut self, _: &mut gpui::RenderContext<Self>) -> gpui::ElementBox {
+        let image_buffer = self.image_buffer.clone();
+        let canvas = Canvas::new(move |bounds, _, cx| {
+            if let Some(image_buffer) = image_buffer.clone() {
+                cx.scene.push_surface(Surface {
+                    bounds,
+                    image_buffer,
+                });
+            }
+        });
+
+        if let Some(image_buffer) = self.image_buffer.as_ref() {
+            canvas
+                .constrained()
+                .with_width(image_buffer.width() as f32)
+                .with_height(image_buffer.height() as f32)
+                .aligned()
+                .boxed()
+        } else {
+            canvas.boxed()
+        }
+    }
+}
+
+fn quit(_: &Quit, cx: &mut gpui::MutableAppContext) {
+    cx.platform().quit();
+}

crates/chat_panel/src/chat_panel.rs 🔗

@@ -308,7 +308,7 @@ impl ChatPanel {
         enum SignInPromptLabel {}
 
         Align::new(
-            MouseEventHandler::new::<SignInPromptLabel, _, _>(0, cx, |mouse_state, _| {
+            MouseEventHandler::<SignInPromptLabel>::new(0, cx, |mouse_state, _| {
                 Label::new(
                     "Sign in to use chat".to_string(),
                     if mouse_state.hovered {

crates/collab/src/integration_tests.rs 🔗

@@ -298,7 +298,8 @@ async fn test_host_disconnect(
     let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
     assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
 
-    let (_, workspace_b) = cx_b.add_window(|cx| Workspace::new(project_b.clone(), cx));
+    let (_, workspace_b) =
+        cx_b.add_window(|cx| Workspace::new(project_b.clone(), |_, _| unimplemented!(), cx));
     let editor_b = workspace_b
         .update(cx_b, |workspace, cx| {
             workspace.open_path((worktree_id, "b.txt"), true, cx)
@@ -2786,7 +2787,8 @@ async fn test_collaborating_with_code_actions(
 
     // Join the project as client B.
     let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
-    let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(project_b.clone(), cx));
+    let (_window_b, workspace_b) =
+        cx_b.add_window(|cx| Workspace::new(project_b.clone(), |_, _| unimplemented!(), cx));
     let editor_b = workspace_b
         .update(cx_b, |workspace, cx| {
             workspace.open_path((worktree_id, "main.rs"), true, cx)
@@ -3001,7 +3003,8 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
     let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
     let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
 
-    let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(project_b.clone(), cx));
+    let (_window_b, workspace_b) =
+        cx_b.add_window(|cx| Workspace::new(project_b.clone(), |_, _| unimplemented!(), cx));
     let editor_b = workspace_b
         .update(cx_b, |workspace, cx| {
             workspace.open_path((worktree_id, "one.rs"), true, cx)
@@ -5224,6 +5227,7 @@ impl TestServer {
             fs: fs.clone(),
             build_window_options: Default::default,
             initialize_workspace: |_, _, _| unimplemented!(),
+            default_item_factory: |_, _| unimplemented!(),
         });
 
         Channel::init(&client);
@@ -5459,7 +5463,9 @@ impl TestClient {
         cx: &mut TestAppContext,
     ) -> ViewHandle<Workspace> {
         let (_, root_view) = cx.add_window(|_| EmptyView);
-        cx.add_view(&root_view, |cx| Workspace::new(project.clone(), cx))
+        cx.add_view(&root_view, |cx| {
+            Workspace::new(project.clone(), |_, _| unimplemented!(), cx)
+        })
     }
 
     async fn simulate_host(

crates/command_palette/src/command_palette.rs 🔗

@@ -350,7 +350,8 @@ mod tests {
         });
 
         let project = Project::test(app_state.fs.clone(), [], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+        let (_, workspace) =
+            cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
         let editor = cx.add_view(&workspace, |cx| {
             let mut editor = Editor::single_line(None, cx);
             editor.set_text("abc", cx);

crates/contacts_panel/src/contacts_panel.rs 🔗

@@ -276,7 +276,7 @@ impl ContactsPanel {
             Section::Offline => "Offline",
         };
         let icon_size = theme.section_icon_size;
-        MouseEventHandler::new::<Header, _, _>(section as usize, cx, |_, _| {
+        MouseEventHandler::<Header>::new(section as usize, cx, |_, _| {
             Flex::row()
                 .with_child(
                     Svg::new(if is_collapsed {
@@ -375,7 +375,7 @@ impl ContactsPanel {
         let baseline_offset =
             row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.;
 
-        MouseEventHandler::new::<JoinProject, _, _>(project_id as usize, cx, |mouse_state, cx| {
+        MouseEventHandler::<JoinProject>::new(project_id as usize, cx, |mouse_state, cx| {
             let tree_branch = *tree_branch.style_for(mouse_state, is_selected);
             let row = theme.project_row.style_for(mouse_state, is_selected);
 
@@ -424,7 +424,7 @@ impl ContactsPanel {
                                 return None;
                             }
 
-                            let button = MouseEventHandler::new::<ToggleProjectOnline, _, _>(
+                            let button = MouseEventHandler::<ToggleProjectOnline>::new(
                                 project_id as usize,
                                 cx,
                                 |state, _| {
@@ -529,7 +529,7 @@ impl ContactsPanel {
         enum ToggleOnline {}
 
         let project_id = project_handle.id();
-        MouseEventHandler::new::<LocalProject, _, _>(project_id, cx, |state, cx| {
+        MouseEventHandler::<LocalProject>::new(project_id, cx, |state, cx| {
             let row = theme.project_row.style_for(state, is_selected);
             let mut worktree_root_names = String::new();
             let project = if let Some(project) = project_handle.upgrade(cx.deref_mut()) {
@@ -548,7 +548,7 @@ impl ContactsPanel {
             Flex::row()
                 .with_child({
                     let button =
-                        MouseEventHandler::new::<ToggleOnline, _, _>(project_id, cx, |state, _| {
+                        MouseEventHandler::<ToggleOnline>::new(project_id, cx, |state, _| {
                             let mut style = *theme.private_button.style_for(state, false);
                             if is_going_online {
                                 style.color = theme.disabled_button.color;
@@ -636,7 +636,7 @@ impl ContactsPanel {
 
         if is_incoming {
             row.add_children([
-                MouseEventHandler::new::<Decline, _, _>(user.id as usize, cx, |mouse_state, _| {
+                MouseEventHandler::<Decline>::new(user.id as usize, cx, |mouse_state, _| {
                     let button_style = if is_contact_request_pending {
                         &theme.disabled_button
                     } else {
@@ -658,7 +658,7 @@ impl ContactsPanel {
                 .contained()
                 .with_margin_right(button_spacing)
                 .boxed(),
-                MouseEventHandler::new::<Accept, _, _>(user.id as usize, cx, |mouse_state, _| {
+                MouseEventHandler::<Accept>::new(user.id as usize, cx, |mouse_state, _| {
                     let button_style = if is_contact_request_pending {
                         &theme.disabled_button
                     } else {
@@ -680,7 +680,7 @@ impl ContactsPanel {
             ]);
         } else {
             row.add_child(
-                MouseEventHandler::new::<Cancel, _, _>(user.id as usize, cx, |mouse_state, _| {
+                MouseEventHandler::<Cancel>::new(user.id as usize, cx, |mouse_state, _| {
                     let button_style = if is_contact_request_pending {
                         &theme.disabled_button
                     } else {
@@ -1071,7 +1071,7 @@ impl View for ContactsPanel {
                                 .boxed(),
                         )
                         .with_child(
-                            MouseEventHandler::new::<AddContact, _, _>(0, cx, |_, _| {
+                            MouseEventHandler::<AddContact>::new(0, cx, |_, _| {
                                 Svg::new("icons/user_plus_16.svg")
                                     .with_color(theme.add_contact_button.color)
                                     .constrained()
@@ -1102,35 +1102,31 @@ impl View for ContactsPanel {
 
                             if info.count > 0 {
                                 Some(
-                                    MouseEventHandler::new::<InviteLink, _, _>(
-                                        0,
-                                        cx,
-                                        |state, cx| {
-                                            let style =
-                                                theme.invite_row.style_for(state, false).clone();
-
-                                            let copied =
-                                                cx.read_from_clipboard().map_or(false, |item| {
-                                                    item.text().as_str() == info.url.as_ref()
-                                                });
-
-                                            Label::new(
-                                                format!(
-                                                    "{} invite link ({} left)",
-                                                    if copied { "Copied" } else { "Copy" },
-                                                    info.count
-                                                ),
-                                                style.label.clone(),
-                                            )
-                                            .aligned()
-                                            .left()
-                                            .constrained()
-                                            .with_height(theme.row_height)
-                                            .contained()
-                                            .with_style(style.container)
-                                            .boxed()
-                                        },
-                                    )
+                                    MouseEventHandler::<InviteLink>::new(0, cx, |state, cx| {
+                                        let style =
+                                            theme.invite_row.style_for(state, false).clone();
+
+                                        let copied =
+                                            cx.read_from_clipboard().map_or(false, |item| {
+                                                item.text().as_str() == info.url.as_ref()
+                                            });
+
+                                        Label::new(
+                                            format!(
+                                                "{} invite link ({} left)",
+                                                if copied { "Copied" } else { "Copy" },
+                                                info.count
+                                            ),
+                                            style.label.clone(),
+                                        )
+                                        .aligned()
+                                        .left()
+                                        .constrained()
+                                        .with_height(theme.row_height)
+                                        .contained()
+                                        .with_style(style.container)
+                                        .boxed()
+                                    })
                                     .with_cursor_style(CursorStyle::PointingHand)
                                     .on_click(MouseButton::Left, move |_, cx| {
                                         cx.write_to_clipboard(ClipboardItem::new(
@@ -1247,7 +1243,8 @@ mod tests {
             .0
             .read_with(cx, |worktree, _| worktree.id().to_proto());
 
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
+        let (_, workspace) =
+            cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
         let panel = cx.add_view(&workspace, |cx| {
             ContactsPanel::new(
                 user_store.clone(),

crates/contacts_panel/src/notifications.rs 🔗

@@ -52,7 +52,7 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
                     .boxed(),
                 )
                 .with_child(
-                    MouseEventHandler::new::<Dismiss, _, _>(user.id as usize, cx, |state, _| {
+                    MouseEventHandler::<Dismiss>::new(user.id as usize, cx, |state, _| {
                         render_icon_button(
                             theme.dismiss_button.style_for(state, false),
                             "icons/x_mark_thin_8.svg",
@@ -90,7 +90,7 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
                 Flex::row()
                     .with_children(buttons.into_iter().enumerate().map(
                         |(ix, (message, action))| {
-                            MouseEventHandler::new::<Button, _, _>(ix, cx, |state, _| {
+                            MouseEventHandler::<Button>::new(ix, cx, |state, _| {
                                 let button = theme.button.style_for(state, false);
                                 Label::new(message.to_string(), button.text.clone())
                                     .contained()

crates/contacts_status_item/Cargo.toml 🔗

@@ -0,0 +1,32 @@
+[package]
+name = "contacts_status_item"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+path = "src/contacts_status_item.rs"
+doctest = false
+
+[dependencies]
+client = { path = "../client" }
+collections = { path = "../collections" }
+editor = { path = "../editor" }
+fuzzy = { path = "../fuzzy" }
+gpui = { path = "../gpui" }
+menu = { path = "../menu" }
+picker = { path = "../picker" }
+project = { path = "../project" }
+settings = { path = "../settings" }
+theme = { path = "../theme" }
+util = { path = "../util" }
+workspace = { path = "../workspace" }
+anyhow = "1.0"
+futures = "0.3"
+log = "0.4"
+postage = { version = "0.4.1", features = ["futures-traits"] }
+serde = { version = "1.0", features = ["derive", "rc"] }
+
+[dev-dependencies]
+language = { path = "../language", features = ["test-support"] }
+project = { path = "../project", features = ["test-support"] }
+workspace = { path = "../workspace", features = ["test-support"] }

crates/contacts_status_item/src/contacts_popover.rs 🔗

@@ -0,0 +1,94 @@
+use editor::Editor;
+use gpui::{elements::*, Entity, RenderContext, View, ViewContext, ViewHandle};
+use settings::Settings;
+
+pub enum Event {
+    Deactivated,
+}
+
+pub struct ContactsPopover {
+    filter_editor: ViewHandle<Editor>,
+}
+
+impl Entity for ContactsPopover {
+    type Event = Event;
+}
+
+impl View for ContactsPopover {
+    fn ui_name() -> &'static str {
+        "ContactsPopover"
+    }
+
+    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
+        let theme = &cx.global::<Settings>().theme.contacts_popover;
+
+        Flex::row()
+            .with_child(
+                ChildView::new(self.filter_editor.clone())
+                    .contained()
+                    .with_style(
+                        cx.global::<Settings>()
+                            .theme
+                            .contacts_panel
+                            .user_query_editor
+                            .container,
+                    )
+                    .flex(1., true)
+                    .boxed(),
+            )
+            // .with_child(
+            //     MouseEventHandler::<AddContact>::new(0, cx, |_, _| {
+            //         Svg::new("icons/user_plus_16.svg")
+            //             .with_color(theme.add_contact_button.color)
+            //             .constrained()
+            //             .with_height(16.)
+            //             .contained()
+            //             .with_style(theme.add_contact_button.container)
+            //             .aligned()
+            //             .boxed()
+            //     })
+            //     .with_cursor_style(CursorStyle::PointingHand)
+            //     .on_click(MouseButton::Left, |_, cx| {
+            //         cx.dispatch_action(contact_finder::Toggle)
+            //     })
+            //     .boxed(),
+            // )
+            .constrained()
+            .with_height(
+                cx.global::<Settings>()
+                    .theme
+                    .contacts_panel
+                    .user_query_editor_height,
+            )
+            .aligned()
+            .top()
+            .contained()
+            .with_background_color(theme.background)
+            .with_uniform_padding(4.)
+            .boxed()
+    }
+}
+
+impl ContactsPopover {
+    pub fn new(cx: &mut ViewContext<Self>) -> Self {
+        cx.observe_window_activation(Self::window_activation_changed)
+            .detach();
+
+        let filter_editor = cx.add_view(|cx| {
+            let mut editor = Editor::single_line(
+                Some(|theme| theme.contacts_panel.user_query_editor.clone()),
+                cx,
+            );
+            editor.set_placeholder_text("Filter contacts", cx);
+            editor
+        });
+
+        Self { filter_editor }
+    }
+
+    fn window_activation_changed(&mut self, is_active: bool, cx: &mut ViewContext<Self>) {
+        if !is_active {
+            cx.emit(Event::Deactivated);
+        }
+    }
+}

crates/contacts_status_item/src/contacts_status_item.rs 🔗

@@ -0,0 +1,94 @@
+mod contacts_popover;
+
+use contacts_popover::ContactsPopover;
+use gpui::{
+    actions,
+    color::Color,
+    elements::*,
+    geometry::{rect::RectF, vector::vec2f},
+    Appearance, Entity, MouseButton, MutableAppContext, RenderContext, View, ViewContext,
+    ViewHandle, WindowKind,
+};
+
+actions!(contacts_status_item, [ToggleContactsPopover]);
+
+pub fn init(cx: &mut MutableAppContext) {
+    cx.add_action(ContactsStatusItem::toggle_contacts_popover);
+}
+
+pub struct ContactsStatusItem {
+    popover: Option<ViewHandle<ContactsPopover>>,
+}
+
+impl Entity for ContactsStatusItem {
+    type Event = ();
+}
+
+impl View for ContactsStatusItem {
+    fn ui_name() -> &'static str {
+        "ContactsStatusItem"
+    }
+
+    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
+        let color = match cx.appearance {
+            Appearance::Light | Appearance::VibrantLight => Color::black(),
+            Appearance::Dark | Appearance::VibrantDark => Color::white(),
+        };
+        MouseEventHandler::<Self>::new(0, cx, |_, _| {
+            Svg::new("icons/zed_22.svg")
+                .with_color(color)
+                .aligned()
+                .boxed()
+        })
+        .on_click(MouseButton::Left, |_, cx| {
+            cx.dispatch_action(ToggleContactsPopover);
+        })
+        .boxed()
+    }
+}
+
+impl ContactsStatusItem {
+    pub fn new() -> Self {
+        Self { popover: None }
+    }
+
+    fn toggle_contacts_popover(&mut self, _: &ToggleContactsPopover, cx: &mut ViewContext<Self>) {
+        match self.popover.take() {
+            Some(popover) => {
+                cx.remove_window(popover.window_id());
+            }
+            None => {
+                let window_bounds = cx.window_bounds();
+                let size = vec2f(360., 460.);
+                let origin = window_bounds.lower_left()
+                    + vec2f(window_bounds.width() / 2. - size.x() / 2., 0.);
+                let (_, popover) = cx.add_window(
+                    gpui::WindowOptions {
+                        bounds: gpui::WindowBounds::Fixed(RectF::new(origin, size)),
+                        titlebar: None,
+                        center: false,
+                        kind: WindowKind::PopUp,
+                        is_movable: false,
+                    },
+                    |cx| ContactsPopover::new(cx),
+                );
+                cx.subscribe(&popover, Self::on_popover_event).detach();
+                self.popover = Some(popover);
+            }
+        }
+    }
+
+    fn on_popover_event(
+        &mut self,
+        popover: ViewHandle<ContactsPopover>,
+        event: &contacts_popover::Event,
+        cx: &mut ViewContext<Self>,
+    ) {
+        match event {
+            contacts_popover::Event::Deactivated => {
+                self.popover.take();
+                cx.remove_window(popover.window_id());
+            }
+        }
+    }
+}

crates/context_menu/src/context_menu.rs 🔗

@@ -22,7 +22,6 @@ pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(ContextMenu::cancel);
 }
 
-//
 pub enum ContextMenuItem {
     Item {
         label: String,
@@ -57,7 +56,8 @@ impl ContextMenuItem {
 
 pub struct ContextMenu {
     show_count: usize,
-    position: Vector2F,
+    anchor_position: Vector2F,
+    anchor_corner: AnchorCorner,
     items: Vec<ContextMenuItem>,
     selected_index: Option<usize>,
     visible: bool,
@@ -100,9 +100,10 @@ impl View for ContextMenu {
             .boxed();
 
         Overlay::new(expanded_menu)
-            .hoverable(true)
-            .fit_mode(OverlayFitMode::SnapToWindow)
-            .with_abs_position(self.position)
+            .with_hoverable(true)
+            .with_fit_mode(OverlayFitMode::SnapToWindow)
+            .with_anchor_position(self.anchor_position)
+            .with_anchor_corner(self.anchor_corner)
             .boxed()
     }
 
@@ -115,7 +116,8 @@ impl ContextMenu {
     pub fn new(cx: &mut ViewContext<Self>) -> Self {
         Self {
             show_count: 0,
-            position: Default::default(),
+            anchor_position: Default::default(),
+            anchor_corner: AnchorCorner::TopLeft,
             items: Default::default(),
             selected_index: Default::default(),
             visible: Default::default(),
@@ -226,14 +228,16 @@ impl ContextMenu {
 
     pub fn show(
         &mut self,
-        position: Vector2F,
+        anchor_position: Vector2F,
+        anchor_corner: AnchorCorner,
         items: impl IntoIterator<Item = ContextMenuItem>,
         cx: &mut ViewContext<Self>,
     ) {
         let mut items = items.into_iter().peekable();
         if items.peek().is_some() {
             self.items = items.collect();
-            self.position = position;
+            self.anchor_position = anchor_position;
+            self.anchor_corner = anchor_corner;
             self.visible = true;
             self.show_count += 1;
             if !cx.is_self_focused() {
@@ -310,13 +314,13 @@ impl ContextMenu {
         enum Menu {}
         enum MenuItem {}
         let style = cx.global::<Settings>().theme.context_menu.clone();
-        MouseEventHandler::new::<Menu, _, _>(0, cx, |_, cx| {
+        MouseEventHandler::<Menu>::new(0, cx, |_, cx| {
             Flex::column()
                 .with_children(self.items.iter().enumerate().map(|(ix, item)| {
                     match item {
                         ContextMenuItem::Item { label, action } => {
                             let action = action.boxed_clone();
-                            MouseEventHandler::new::<MenuItem, _, _>(ix, cx, |state, _| {
+                            MouseEventHandler::<MenuItem>::new(ix, cx, |state, _| {
                                 let style =
                                     style.item.style_for(state, Some(ix) == self.selected_index);
 

crates/diagnostics/src/diagnostics.rs 🔗

@@ -776,7 +776,8 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
+        let (_, workspace) =
+            cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
 
         // Create some diagnostics
         project.update(cx, |project, cx| {
@@ -1149,7 +1150,7 @@ mod tests {
         editor: &ViewHandle<Editor>,
         cx: &mut MutableAppContext,
     ) -> Vec<(u32, String)> {
-        let mut presenter = cx.build_presenter(editor.id(), 0.);
+        let mut presenter = cx.build_presenter(editor.id(), 0., Default::default());
         let mut cx = presenter.build_layout_context(Default::default(), false, cx);
         cx.render(editor, |editor, cx| {
             let snapshot = editor.snapshot(cx);

crates/diagnostics/src/items.rs 🔗

@@ -89,7 +89,7 @@ impl View for DiagnosticIndicator {
         let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
         let in_progress = !self.in_progress_checks.is_empty();
         let mut element = Flex::row().with_child(
-            MouseEventHandler::new::<Summary, _, _>(0, cx, |state, cx| {
+            MouseEventHandler::<Summary>::new(0, cx, |state, cx| {
                 let style = cx
                     .global::<Settings>()
                     .theme
@@ -190,7 +190,7 @@ impl View for DiagnosticIndicator {
         } else if let Some(diagnostic) = &self.current_diagnostic {
             let message_style = style.diagnostic_message.clone();
             element.add_child(
-                MouseEventHandler::new::<Message, _, _>(1, cx, |state, _| {
+                MouseEventHandler::<Message>::new(1, cx, |state, _| {
                     Label::new(
                         diagnostic.message.split('\n').next().unwrap().to_string(),
                         message_style.style_for(state, false).text.clone(),

crates/drag_and_drop/src/drag_and_drop.rs 🔗

@@ -2,7 +2,7 @@ use std::{any::Any, rc::Rc};
 
 use collections::HashSet;
 use gpui::{
-    elements::{Container, MouseEventHandler},
+    elements::{MouseEventHandler, Overlay},
     geometry::vector::Vector2F,
     scene::DragRegionEvent,
     CursorStyle, Element, ElementBox, EventContext, MouseButton, MutableAppContext, RenderContext,
@@ -114,30 +114,29 @@ impl<V: View> DragAndDrop<V> {
 
                 let position = position + region_offset;
 
+                enum DraggedElementHandler {}
                 Some(
-                    MouseEventHandler::new::<Self, _, _>(0, cx, |_, cx| {
-                        Container::new(render(payload, cx))
-                            .with_margin_left(position.x())
-                            .with_margin_top(position.y())
-                            .aligned()
-                            .top()
-                            .left()
-                            .boxed()
-                    })
-                    .with_cursor_style(CursorStyle::Arrow)
-                    .on_up(MouseButton::Left, |_, cx| {
-                        cx.defer(|cx| {
-                            cx.update_global::<Self, _, _>(|this, cx| this.stop_dragging(cx));
-                        });
-                        cx.propogate_event();
-                    })
-                    .on_up_out(MouseButton::Left, |_, cx| {
-                        cx.defer(|cx| {
-                            cx.update_global::<Self, _, _>(|this, cx| this.stop_dragging(cx));
-                        });
-                    })
-                    // Don't block hover events or invalidations
-                    .with_hoverable(false)
+                    Overlay::new(
+                        MouseEventHandler::<DraggedElementHandler>::new(0, cx, |_, cx| {
+                            render(payload, cx)
+                        })
+                        .with_cursor_style(CursorStyle::Arrow)
+                        .on_up(MouseButton::Left, |_, cx| {
+                            cx.defer(|cx| {
+                                cx.update_global::<Self, _, _>(|this, cx| this.stop_dragging(cx));
+                            });
+                            cx.propogate_event();
+                        })
+                        .on_up_out(MouseButton::Left, |_, cx| {
+                            cx.defer(|cx| {
+                                cx.update_global::<Self, _, _>(|this, cx| this.stop_dragging(cx));
+                            });
+                        })
+                        // Don't block hover events or invalidations
+                        .with_hoverable(false)
+                        .boxed(),
+                    )
+                    .with_anchor_position(position)
                     .boxed(),
                 )
             },
@@ -174,7 +173,7 @@ pub trait Draggable {
         Self: Sized;
 }
 
-impl Draggable for MouseEventHandler {
+impl<Tag> Draggable for MouseEventHandler<Tag> {
     fn as_draggable<V: View, P: Any>(
         self,
         payload: P,

crates/editor/src/editor.rs 🔗

@@ -682,7 +682,7 @@ impl CompletionsMenu {
                     let completion = &completions[mat.candidate_id];
                     let item_ix = start_ix + ix;
                     items.push(
-                        MouseEventHandler::new::<CompletionTag, _, _>(
+                        MouseEventHandler::<CompletionTag>::new(
                             mat.candidate_id,
                             cx,
                             |state, _| {
@@ -830,7 +830,7 @@ impl CodeActionsMenu {
                 for (ix, action) in actions[range].iter().enumerate() {
                     let item_ix = start_ix + ix;
                     items.push(
-                        MouseEventHandler::new::<ActionTag, _, _>(item_ix, cx, |state, _| {
+                        MouseEventHandler::<ActionTag>::new(item_ix, cx, |state, _| {
                             let item_style = if item_ix == selected_item {
                                 style.autocomplete.selected_item
                             } else if state.hovered {
@@ -1575,17 +1575,20 @@ impl Editor {
         let start;
         let end;
         let mode;
+        let auto_scroll;
         match click_count {
             1 => {
                 start = buffer.anchor_before(position.to_point(&display_map));
                 end = start.clone();
                 mode = SelectMode::Character;
+                auto_scroll = true;
             }
             2 => {
                 let range = movement::surrounding_word(&display_map, position);
                 start = buffer.anchor_before(range.start.to_point(&display_map));
                 end = buffer.anchor_before(range.end.to_point(&display_map));
                 mode = SelectMode::Word(start.clone()..end.clone());
+                auto_scroll = true;
             }
             3 => {
                 let position = display_map
@@ -1599,15 +1602,17 @@ impl Editor {
                 start = buffer.anchor_before(line_start);
                 end = buffer.anchor_before(next_line_start);
                 mode = SelectMode::Line(start.clone()..end.clone());
+                auto_scroll = true;
             }
             _ => {
                 start = buffer.anchor_before(0);
                 end = buffer.anchor_before(buffer.len());
                 mode = SelectMode::All;
+                auto_scroll = false;
             }
         }
 
-        self.change_selections(Some(Autoscroll::Newest), cx, |s| {
+        self.change_selections(auto_scroll.then(|| Autoscroll::Newest), cx, |s| {
             if !add {
                 s.clear_disjoint();
             } else if click_count > 1 {
@@ -2735,7 +2740,7 @@ impl Editor {
         if self.available_code_actions.is_some() {
             enum Tag {}
             Some(
-                MouseEventHandler::new::<Tag, _, _>(0, cx, |_, _| {
+                MouseEventHandler::<Tag>::new(0, cx, |_, _| {
                     Svg::new("icons/bolt_8.svg")
                         .with_color(style.code_actions.indicator)
                         .boxed()
@@ -7100,7 +7105,7 @@ mod tests {
     fn test_navigation_history(cx: &mut gpui::MutableAppContext) {
         cx.set_global(Settings::test(cx));
         use workspace::Item;
-        let (_, pane) = cx.add_window(Default::default(), Pane::new);
+        let (_, pane) = cx.add_window(Default::default(), |cx| Pane::new(None, cx));
         let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
 
         cx.add_view(&pane, |cx| {
@@ -7826,7 +7831,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) {
-        let mut cx = EditorTestContext::new(cx).await;
+        let mut cx = EditorTestContext::new(cx);
         cx.set_state("one «two threeˇ» four");
         cx.update_editor(|editor, cx| {
             editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, cx);
@@ -7974,7 +7979,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_newline_below(cx: &mut gpui::TestAppContext) {
-        let mut cx = EditorTestContext::new(cx).await;
+        let mut cx = EditorTestContext::new(cx);
         cx.update(|cx| {
             cx.update_global::<Settings, _, _>(|settings, _| {
                 settings.editor_overrides.tab_size = Some(NonZeroU32::new(4).unwrap());
@@ -8050,7 +8055,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_tab(cx: &mut gpui::TestAppContext) {
-        let mut cx = EditorTestContext::new(cx).await;
+        let mut cx = EditorTestContext::new(cx);
         cx.update(|cx| {
             cx.update_global::<Settings, _, _>(|settings, _| {
                 settings.editor_overrides.tab_size = Some(NonZeroU32::new(3).unwrap());
@@ -8081,7 +8086,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_tab_on_blank_line_auto_indents(cx: &mut gpui::TestAppContext) {
-        let mut cx = EditorTestContext::new(cx).await;
+        let mut cx = EditorTestContext::new(cx);
         let language = Arc::new(
             Language::new(
                 LanguageConfig::default(),
@@ -8139,7 +8144,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
-        let mut cx = EditorTestContext::new(cx).await;
+        let mut cx = EditorTestContext::new(cx);
 
         cx.set_state(indoc! {"
               «oneˇ» «twoˇ»
@@ -8208,7 +8213,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_indent_outdent_with_hard_tabs(cx: &mut gpui::TestAppContext) {
-        let mut cx = EditorTestContext::new(cx).await;
+        let mut cx = EditorTestContext::new(cx);
         cx.update(|cx| {
             cx.update_global::<Settings, _, _>(|settings, _| {
                 settings.editor_overrides.hard_tabs = Some(true);
@@ -8416,7 +8421,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_backspace(cx: &mut gpui::TestAppContext) {
-        let mut cx = EditorTestContext::new(cx).await;
+        let mut cx = EditorTestContext::new(cx);
 
         // Basic backspace
         cx.set_state(indoc! {"
@@ -8463,7 +8468,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_delete(cx: &mut gpui::TestAppContext) {
-        let mut cx = EditorTestContext::new(cx).await;
+        let mut cx = EditorTestContext::new(cx);
 
         cx.set_state(indoc! {"
             onˇe two three
@@ -8800,7 +8805,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_clipboard(cx: &mut gpui::TestAppContext) {
-        let mut cx = EditorTestContext::new(cx).await;
+        let mut cx = EditorTestContext::new(cx);
 
         cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
         cx.update_editor(|e, cx| e.cut(&Cut, cx));
@@ -8876,7 +8881,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_paste_multiline(cx: &mut gpui::TestAppContext) {
-        let mut cx = EditorTestContext::new(cx).await;
+        let mut cx = EditorTestContext::new(cx);
         let language = Arc::new(Language::new(
             LanguageConfig::default(),
             Some(tree_sitter_rust::language()),
@@ -9305,7 +9310,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_select_next(cx: &mut gpui::TestAppContext) {
-        let mut cx = EditorTestContext::new(cx).await;
+        let mut cx = EditorTestContext::new(cx);
         cx.set_state("abc\nˇabc abc\ndefabc\nabc");
 
         cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx));

crates/editor/src/element.rs 🔗

@@ -5,7 +5,9 @@ use super::{
 };
 use crate::{
     display_map::{BlockStyle, DisplaySnapshot, TransformBlock},
-    hover_popover::HoverAt,
+    hover_popover::{
+        HoverAt, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT,
+    },
     link_go_to_definition::{
         CmdShiftChanged, GoToFetchedDefinition, GoToFetchedTypeDefinition, UpdateGoToDefinitionLink,
     },
@@ -28,7 +30,7 @@ use gpui::{
     text_layout::{self, Line, RunStyle, TextLayoutCache},
     AppContext, Axis, Border, CursorRegion, Element, ElementBox, Event, EventContext,
     LayoutContext, ModifiersChangedEvent, MouseButton, MouseButtonEvent, MouseMovedEvent,
-    MutableAppContext, PaintContext, Quad, Scene, ScrollWheelEvent, SizeConstraint, ViewContext,
+    MouseRegion, MutableAppContext, PaintContext, Quad, Scene, SizeConstraint, ViewContext,
     WeakViewHandle,
 };
 use json::json;
@@ -41,12 +43,9 @@ use std::{
     fmt::Write,
     iter,
     ops::Range,
+    sync::Arc,
 };
 
-const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.;
-const MIN_POPOVER_LINE_HEIGHT: f32 = 4.;
-const HOVER_POPOVER_GAP: f32 = 10.;
-
 struct SelectionLayout {
     head: DisplayPoint,
     range: Range<DisplayPoint>,
@@ -76,9 +75,10 @@ impl SelectionLayout {
     }
 }
 
+#[derive(Clone)]
 pub struct EditorElement {
     view: WeakViewHandle<Editor>,
-    style: EditorStyle,
+    style: Arc<EditorStyle>,
     cursor_shape: CursorShape,
 }
 
@@ -90,7 +90,7 @@ impl EditorElement {
     ) -> Self {
         Self {
             view,
-            style,
+            style: Arc::new(style),
             cursor_shape,
         }
     }
@@ -110,8 +110,98 @@ impl EditorElement {
         self.update_view(cx, |view, cx| view.snapshot(cx))
     }
 
+    fn attach_mouse_handlers(
+        view: &WeakViewHandle<Editor>,
+        position_map: &Arc<PositionMap>,
+        visible_bounds: RectF,
+        text_bounds: RectF,
+        gutter_bounds: RectF,
+        bounds: RectF,
+        cx: &mut PaintContext,
+    ) {
+        enum EditorElementMouseHandlers {}
+        cx.scene.push_mouse_region(
+            MouseRegion::new::<EditorElementMouseHandlers>(view.id(), view.id(), visible_bounds)
+                .on_down(MouseButton::Left, {
+                    let position_map = position_map.clone();
+                    move |e, cx| {
+                        if !Self::mouse_down(
+                            e.platform_event,
+                            position_map.as_ref(),
+                            text_bounds,
+                            gutter_bounds,
+                            cx,
+                        ) {
+                            cx.propogate_event();
+                        }
+                    }
+                })
+                .on_down(MouseButton::Right, {
+                    let position_map = position_map.clone();
+                    move |e, cx| {
+                        if !Self::mouse_right_down(
+                            e.position,
+                            position_map.as_ref(),
+                            text_bounds,
+                            cx,
+                        ) {
+                            cx.propogate_event();
+                        }
+                    }
+                })
+                .on_up(MouseButton::Left, {
+                    let view = view.clone();
+                    let position_map = position_map.clone();
+                    move |e, cx| {
+                        if !Self::mouse_up(
+                            view.clone(),
+                            e.position,
+                            e.cmd,
+                            e.shift,
+                            position_map.as_ref(),
+                            text_bounds,
+                            cx,
+                        ) {
+                            cx.propogate_event()
+                        }
+                    }
+                })
+                .on_drag(MouseButton::Left, {
+                    let view = view.clone();
+                    let position_map = position_map.clone();
+                    move |e, cx| {
+                        if !Self::mouse_dragged(
+                            view.clone(),
+                            e.platform_event,
+                            position_map.as_ref(),
+                            text_bounds,
+                            cx,
+                        ) {
+                            cx.propogate_event()
+                        }
+                    }
+                })
+                .on_move({
+                    let position_map = position_map.clone();
+                    move |e, cx| {
+                        if !Self::mouse_moved(e.platform_event, &position_map, text_bounds, cx) {
+                            cx.propogate_event()
+                        }
+                    }
+                })
+                .on_scroll({
+                    let position_map = position_map.clone();
+                    move |e, cx| {
+                        if !Self::scroll(e.position, e.delta, e.precise, &position_map, bounds, cx)
+                        {
+                            cx.propogate_event()
+                        }
+                    }
+                }),
+        );
+    }
+
     fn mouse_down(
-        &self,
         MouseButtonEvent {
             position,
             ctrl,
@@ -121,18 +211,18 @@ impl EditorElement {
             mut click_count,
             ..
         }: MouseButtonEvent,
-        layout: &mut LayoutState,
-        paint: &mut PaintState,
+        position_map: &PositionMap,
+        text_bounds: RectF,
+        gutter_bounds: RectF,
         cx: &mut EventContext,
     ) -> bool {
-        if paint.gutter_bounds.contains_point(position) {
+        if gutter_bounds.contains_point(position) {
             click_count = 3; // Simulate triple-click when clicking the gutter to select lines
-        } else if !paint.text_bounds.contains_point(position) {
+        } else if !text_bounds.contains_point(position) {
             return false;
         }
 
-        let snapshot = self.snapshot(cx.app);
-        let (position, target_position) = paint.point_for_position(&snapshot, layout, position);
+        let (position, target_position) = position_map.point_for_position(text_bounds, position);
 
         if shift && alt {
             cx.dispatch_action(Select(SelectPhase::BeginColumnar {
@@ -156,33 +246,31 @@ impl EditorElement {
     }
 
     fn mouse_right_down(
-        &self,
         position: Vector2F,
-        layout: &mut LayoutState,
-        paint: &mut PaintState,
+        position_map: &PositionMap,
+        text_bounds: RectF,
         cx: &mut EventContext,
     ) -> bool {
-        if !paint.text_bounds.contains_point(position) {
+        if !text_bounds.contains_point(position) {
             return false;
         }
 
-        let snapshot = self.snapshot(cx.app);
-        let (point, _) = paint.point_for_position(&snapshot, layout, position);
+        let (point, _) = position_map.point_for_position(text_bounds, position);
 
         cx.dispatch_action(DeployMouseContextMenu { position, point });
         true
     }
 
     fn mouse_up(
-        &self,
+        view: WeakViewHandle<Editor>,
         position: Vector2F,
         cmd: bool,
         shift: bool,
-        layout: &mut LayoutState,
-        paint: &mut PaintState,
+        position_map: &PositionMap,
+        text_bounds: RectF,
         cx: &mut EventContext,
     ) -> bool {
-        let view = self.view(cx.app.as_ref());
+        let view = view.upgrade(cx.app).unwrap().read(cx.app);
         let end_selection = view.has_pending_selection();
         let pending_nonempty_selections = view.has_pending_nonempty_selection();
 
@@ -190,9 +278,8 @@ impl EditorElement {
             cx.dispatch_action(Select(SelectPhase::End));
         }
 
-        if !pending_nonempty_selections && cmd && paint.text_bounds.contains_point(position) {
-            let (point, target_point) =
-                paint.point_for_position(&self.snapshot(cx), layout, position);
+        if !pending_nonempty_selections && cmd && text_bounds.contains_point(position) {
+            let (point, target_point) = position_map.point_for_position(text_bounds, position);
 
             if point == target_point {
                 if shift {
@@ -209,22 +296,21 @@ impl EditorElement {
     }
 
     fn mouse_dragged(
-        &self,
+        view: WeakViewHandle<Editor>,
         MouseMovedEvent {
             cmd,
             shift,
             position,
             ..
         }: MouseMovedEvent,
-        layout: &mut LayoutState,
-        paint: &mut PaintState,
+        position_map: &PositionMap,
+        text_bounds: RectF,
         cx: &mut EventContext,
     ) -> bool {
         // This will be handled more correctly once https://github.com/zed-industries/zed/issues/1218 is completed
         // Don't trigger hover popover if mouse is hovering over context menu
-        let point = if paint.text_bounds.contains_point(position) {
-            let (point, target_point) =
-                paint.point_for_position(&self.snapshot(cx), layout, position);
+        let point = if text_bounds.contains_point(position) {
+            let (point, target_point) = position_map.point_for_position(text_bounds, position);
             if point == target_point {
                 Some(point)
             } else {
@@ -240,14 +326,13 @@ impl EditorElement {
             shift_held: shift,
         });
 
-        let view = self.view(cx.app);
+        let view = view.upgrade(cx.app).unwrap().read(cx.app);
         if view.has_pending_selection() {
-            let rect = paint.text_bounds;
             let mut scroll_delta = Vector2F::zero();
 
-            let vertical_margin = layout.line_height.min(rect.height() / 3.0);
-            let top = rect.origin_y() + vertical_margin;
-            let bottom = rect.lower_left().y() - vertical_margin;
+            let vertical_margin = position_map.line_height.min(text_bounds.height() / 3.0);
+            let top = text_bounds.origin_y() + vertical_margin;
+            let bottom = text_bounds.lower_left().y() - vertical_margin;
             if position.y() < top {
                 scroll_delta.set_y(-scale_vertical_mouse_autoscroll_delta(top - position.y()))
             }
@@ -255,9 +340,9 @@ impl EditorElement {
                 scroll_delta.set_y(scale_vertical_mouse_autoscroll_delta(position.y() - bottom))
             }
 
-            let horizontal_margin = layout.line_height.min(rect.width() / 3.0);
-            let left = rect.origin_x() + horizontal_margin;
-            let right = rect.upper_right().x() - horizontal_margin;
+            let horizontal_margin = position_map.line_height.min(text_bounds.width() / 3.0);
+            let left = text_bounds.origin_x() + horizontal_margin;
+            let right = text_bounds.upper_right().x() - horizontal_margin;
             if position.x() < left {
                 scroll_delta.set_x(-scale_horizontal_mouse_autoscroll_delta(
                     left - position.x(),
@@ -269,14 +354,14 @@ impl EditorElement {
                 ))
             }
 
-            let snapshot = self.snapshot(cx.app);
-            let (position, target_position) = paint.point_for_position(&snapshot, layout, position);
+            let (position, target_position) =
+                position_map.point_for_position(text_bounds, position);
 
             cx.dispatch_action(Select(SelectPhase::Update {
                 position,
                 goal_column: target_position.column(),
-                scroll_position: (snapshot.scroll_position() + scroll_delta)
-                    .clamp(Vector2F::zero(), layout.scroll_max),
+                scroll_position: (position_map.snapshot.scroll_position() + scroll_delta)
+                    .clamp(Vector2F::zero(), position_map.scroll_max),
             }));
 
             cx.dispatch_action(HoverAt { point });
@@ -288,22 +373,20 @@ impl EditorElement {
     }
 
     fn mouse_moved(
-        &self,
         MouseMovedEvent {
             cmd,
             shift,
             position,
             ..
         }: MouseMovedEvent,
-        layout: &LayoutState,
-        paint: &PaintState,
+        position_map: &PositionMap,
+        text_bounds: RectF,
         cx: &mut EventContext,
     ) -> bool {
         // This will be handled more correctly once https://github.com/zed-industries/zed/issues/1218 is completed
         // Don't trigger hover popover if mouse is hovering over context menu
-        let point = if paint.text_bounds.contains_point(position) {
-            let (point, target_point) =
-                paint.point_for_position(&self.snapshot(cx), layout, position);
+        let point = if text_bounds.contains_point(position) {
+            let (point, target_point) = position_map.point_for_position(text_bounds, position);
             if point == target_point {
                 Some(point)
             } else {
@@ -319,23 +402,6 @@ impl EditorElement {
             shift_held: shift,
         });
 
-        if paint
-            .context_menu_bounds
-            .map_or(false, |context_menu_bounds| {
-                context_menu_bounds.contains_point(position)
-            })
-        {
-            return false;
-        }
-
-        if paint
-            .hover_popover_bounds
-            .iter()
-            .any(|hover_bounds| hover_bounds.contains_point(position))
-        {
-            return false;
-        }
-
         cx.dispatch_action(HoverAt { point });
         true
     }
@@ -349,28 +415,27 @@ impl EditorElement {
     }
 
     fn scroll(
-        &self,
         position: Vector2F,
         mut delta: Vector2F,
         precise: bool,
-        layout: &mut LayoutState,
-        paint: &mut PaintState,
+        position_map: &PositionMap,
+        bounds: RectF,
         cx: &mut EventContext,
     ) -> bool {
-        if !paint.bounds.contains_point(position) {
+        if !bounds.contains_point(position) {
             return false;
         }
 
-        let snapshot = self.snapshot(cx.app);
-        let max_glyph_width = layout.em_width;
+        let max_glyph_width = position_map.em_width;
         if !precise {
-            delta *= vec2f(max_glyph_width, layout.line_height);
+            delta *= vec2f(max_glyph_width, position_map.line_height);
         }
 
-        let scroll_position = snapshot.scroll_position();
+        let scroll_position = position_map.snapshot.scroll_position();
         let x = (scroll_position.x() * max_glyph_width - delta.x()) / max_glyph_width;
-        let y = (scroll_position.y() * layout.line_height - delta.y()) / layout.line_height;
-        let scroll_position = vec2f(x, y).clamp(Vector2F::zero(), layout.scroll_max);
+        let y =
+            (scroll_position.y() * position_map.line_height - delta.y()) / position_map.line_height;
+        let scroll_position = vec2f(x, y).clamp(Vector2F::zero(), position_map.scroll_max);
 
         cx.dispatch_action(Scroll(scroll_position));
 
@@ -385,7 +450,8 @@ impl EditorElement {
         cx: &mut PaintContext,
     ) {
         let bounds = gutter_bounds.union_rect(text_bounds);
-        let scroll_top = layout.snapshot.scroll_position().y() * layout.line_height;
+        let scroll_top =
+            layout.position_map.snapshot.scroll_position().y() * layout.position_map.line_height;
         let editor = self.view(cx.app);
         cx.scene.push_quad(Quad {
             bounds: gutter_bounds,
@@ -414,11 +480,12 @@ impl EditorElement {
                 if !contains_non_empty_selection {
                     let origin = vec2f(
                         bounds.origin_x(),
-                        bounds.origin_y() + (layout.line_height * *start_row as f32) - scroll_top,
+                        bounds.origin_y() + (layout.position_map.line_height * *start_row as f32)
+                            - scroll_top,
                     );
                     let size = vec2f(
                         bounds.width(),
-                        layout.line_height * (end_row - start_row + 1) as f32,
+                        layout.position_map.line_height * (end_row - start_row + 1) as f32,
                     );
                     cx.scene.push_quad(Quad {
                         bounds: RectF::new(origin, size),
@@ -432,12 +499,13 @@ impl EditorElement {
             if let Some(highlighted_rows) = &layout.highlighted_rows {
                 let origin = vec2f(
                     bounds.origin_x(),
-                    bounds.origin_y() + (layout.line_height * highlighted_rows.start as f32)
+                    bounds.origin_y()
+                        + (layout.position_map.line_height * highlighted_rows.start as f32)
                         - scroll_top,
                 );
                 let size = vec2f(
                     bounds.width(),
-                    layout.line_height * highlighted_rows.len() as f32,
+                    layout.position_map.line_height * highlighted_rows.len() as f32,
                 );
                 cx.scene.push_quad(Quad {
                     bounds: RectF::new(origin, size),
@@ -456,23 +524,30 @@ impl EditorElement {
         layout: &mut LayoutState,
         cx: &mut PaintContext,
     ) {
-        let scroll_top = layout.snapshot.scroll_position().y() * layout.line_height;
+        let scroll_top =
+            layout.position_map.snapshot.scroll_position().y() * layout.position_map.line_height;
         for (ix, line) in layout.line_number_layouts.iter().enumerate() {
             if let Some(line) = line {
                 let line_origin = bounds.origin()
                     + vec2f(
                         bounds.width() - line.width() - layout.gutter_padding,
-                        ix as f32 * layout.line_height - (scroll_top % layout.line_height),
+                        ix as f32 * layout.position_map.line_height
+                            - (scroll_top % layout.position_map.line_height),
                     );
-                line.paint(line_origin, visible_bounds, layout.line_height, cx);
+                line.paint(
+                    line_origin,
+                    visible_bounds,
+                    layout.position_map.line_height,
+                    cx,
+                );
             }
         }
 
         if let Some((row, indicator)) = layout.code_actions_indicator.as_mut() {
             let mut x = bounds.width() - layout.gutter_padding;
-            let mut y = *row as f32 * layout.line_height - scroll_top;
+            let mut y = *row as f32 * layout.position_map.line_height - scroll_top;
             x += ((layout.gutter_padding + layout.gutter_margin) - indicator.size().x()) / 2.;
-            y += (layout.line_height - indicator.size().y()) / 2.;
+            y += (layout.position_map.line_height - indicator.size().y()) / 2.;
             indicator.paint(bounds.origin() + vec2f(x, y), visible_bounds, cx);
         }
     }
@@ -482,17 +557,17 @@ impl EditorElement {
         bounds: RectF,
         visible_bounds: RectF,
         layout: &mut LayoutState,
-        paint: &mut PaintState,
         cx: &mut PaintContext,
     ) {
         let view = self.view(cx.app);
         let style = &self.style;
         let local_replica_id = view.replica_id(cx);
-        let scroll_position = layout.snapshot.scroll_position();
+        let scroll_position = layout.position_map.snapshot.scroll_position();
         let start_row = scroll_position.y() as u32;
-        let scroll_top = scroll_position.y() * layout.line_height;
-        let end_row = ((scroll_top + bounds.height()) / layout.line_height).ceil() as u32 + 1; // Add 1 to ensure selections bleed off screen
-        let max_glyph_width = layout.em_width;
+        let scroll_top = scroll_position.y() * layout.position_map.line_height;
+        let end_row =
+            ((scroll_top + bounds.height()) / layout.position_map.line_height).ceil() as u32 + 1; // Add 1 to ensure selections bleed off screen
+        let max_glyph_width = layout.position_map.em_width;
         let scroll_left = scroll_position.x() * max_glyph_width;
         let content_origin = bounds.origin() + vec2f(layout.gutter_margin, 0.);
 
@@ -514,7 +589,7 @@ impl EditorElement {
                 end_row,
                 *color,
                 0.,
-                0.15 * layout.line_height,
+                0.15 * layout.position_map.line_height,
                 layout,
                 content_origin,
                 scroll_top,
@@ -527,7 +602,7 @@ impl EditorElement {
         let mut cursors = SmallVec::<[Cursor; 32]>::new();
         for (replica_id, selections) in &layout.selections {
             let selection_style = style.replica_selection_style(*replica_id);
-            let corner_radius = 0.15 * layout.line_height;
+            let corner_radius = 0.15 * layout.position_map.line_height;
 
             for selection in selections {
                 self.paint_highlighted_range(
@@ -548,50 +623,52 @@ impl EditorElement {
                 if view.show_local_cursors() || *replica_id != local_replica_id {
                     let cursor_position = selection.head;
                     if (start_row..end_row).contains(&cursor_position.row()) {
-                        let cursor_row_layout =
-                            &layout.line_layouts[(cursor_position.row() - start_row) as usize];
+                        let cursor_row_layout = &layout.position_map.line_layouts
+                            [(cursor_position.row() - start_row) as usize];
                         let cursor_column = cursor_position.column() as usize;
 
                         let cursor_character_x = cursor_row_layout.x_for_index(cursor_column);
                         let mut block_width =
                             cursor_row_layout.x_for_index(cursor_column + 1) - cursor_character_x;
                         if block_width == 0.0 {
-                            block_width = layout.em_width;
+                            block_width = layout.position_map.em_width;
                         }
-
-                        let block_text =
-                            if let CursorShape::Block = self.cursor_shape {
-                                layout.snapshot.chars_at(cursor_position).next().and_then(
-                                    |character| {
-                                        let font_id =
-                                            cursor_row_layout.font_for_index(cursor_column)?;
-                                        let text = character.to_string();
-
-                                        Some(cx.text_layout_cache.layout_str(
-                                            &text,
-                                            cursor_row_layout.font_size(),
-                                            &[(
-                                                text.len(),
-                                                RunStyle {
-                                                    font_id,
-                                                    color: style.background,
-                                                    underline: Default::default(),
-                                                },
-                                            )],
-                                        ))
-                                    },
-                                )
-                            } else {
-                                None
-                            };
+                        let block_text = if let CursorShape::Block = self.cursor_shape {
+                            layout
+                                .position_map
+                                .snapshot
+                                .chars_at(cursor_position)
+                                .next()
+                                .and_then(|character| {
+                                    let font_id =
+                                        cursor_row_layout.font_for_index(cursor_column)?;
+                                    let text = character.to_string();
+
+                                    Some(cx.text_layout_cache.layout_str(
+                                        &text,
+                                        cursor_row_layout.font_size(),
+                                        &[(
+                                            text.len(),
+                                            RunStyle {
+                                                font_id,
+                                                color: style.background,
+                                                underline: Default::default(),
+                                            },
+                                        )],
+                                    ))
+                                })
+                        } else {
+                            None
+                        };
 
                         let x = cursor_character_x - scroll_left;
-                        let y = cursor_position.row() as f32 * layout.line_height - scroll_top;
+                        let y = cursor_position.row() as f32 * layout.position_map.line_height
+                            - scroll_top;
                         cursors.push(Cursor {
                             color: selection_style.cursor,
                             block_width,
                             origin: vec2f(x, y),
-                            line_height: layout.line_height,
+                            line_height: layout.position_map.line_height,
                             shape: self.cursor_shape,
                             block_text,
                         });
@@ -602,13 +679,16 @@ impl EditorElement {
 
         if let Some(visible_text_bounds) = bounds.intersection(visible_bounds) {
             // Draw glyphs
-            for (ix, line) in layout.line_layouts.iter().enumerate() {
+            for (ix, line) in layout.position_map.line_layouts.iter().enumerate() {
                 let row = start_row + ix as u32;
                 line.paint(
                     content_origin
-                        + vec2f(-scroll_left, row as f32 * layout.line_height - scroll_top),
+                        + vec2f(
+                            -scroll_left,
+                            row as f32 * layout.position_map.line_height - scroll_top,
+                        ),
                     visible_text_bounds,
-                    layout.line_height,
+                    layout.position_map.line_height,
                     cx,
                 );
             }
@@ -622,9 +702,10 @@ impl EditorElement {
 
         if let Some((position, context_menu)) = layout.context_menu.as_mut() {
             cx.scene.push_stacking_context(None);
-            let cursor_row_layout = &layout.line_layouts[(position.row() - start_row) as usize];
+            let cursor_row_layout =
+                &layout.position_map.line_layouts[(position.row() - start_row) as usize];
             let x = cursor_row_layout.x_for_index(position.column() as usize) - scroll_left;
-            let y = (position.row() + 1) as f32 * layout.line_height - scroll_top;
+            let y = (position.row() + 1) as f32 * layout.position_map.line_height - scroll_top;
             let mut list_origin = content_origin + vec2f(x, y);
             let list_width = context_menu.size().x();
             let list_height = context_menu.size().y();
@@ -636,7 +717,7 @@ impl EditorElement {
             }
 
             if list_origin.y() + list_height > bounds.max_y() {
-                list_origin.set_y(list_origin.y() - layout.line_height - list_height);
+                list_origin.set_y(list_origin.y() - layout.position_map.line_height - list_height);
             }
 
             context_menu.paint(
@@ -645,8 +726,6 @@ impl EditorElement {
                 cx,
             );
 
-            paint.context_menu_bounds = Some(RectF::new(list_origin, context_menu.size()));
-
             cx.scene.pop_stacking_context();
         }
 
@@ -654,22 +733,21 @@ impl EditorElement {
             cx.scene.push_stacking_context(None);
 
             // This is safe because we check on layout whether the required row is available
-            let hovered_row_layout = &layout.line_layouts[(position.row() - start_row) as usize];
+            let hovered_row_layout =
+                &layout.position_map.line_layouts[(position.row() - start_row) as usize];
 
             // 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].size();
-            let height_to_reserve =
-                first_size.y() + 1.5 * MIN_POPOVER_LINE_HEIGHT as f32 * layout.line_height;
+            let height_to_reserve = first_size.y()
+                + 1.5 * MIN_POPOVER_LINE_HEIGHT as f32 * layout.position_map.line_height;
 
             // Compute Hovered Point
             let x = hovered_row_layout.x_for_index(position.column() as usize) - scroll_left;
-            let y = position.row() as f32 * layout.line_height - scroll_top;
+            let y = position.row() as f32 * layout.position_map.line_height - scroll_top;
             let hovered_point = content_origin + vec2f(x, y);
 
-            paint.hover_popover_bounds.clear();
-
             if hovered_point.y() - height_to_reserve > 0.0 {
                 // There is enough space above. Render popovers above the hovered point
                 let mut current_y = hovered_point.y();
@@ -688,16 +766,11 @@ impl EditorElement {
                         cx,
                     );
 
-                    paint.hover_popover_bounds.push(
-                        RectF::new(popover_origin, hover_popover.size())
-                            .dilate(Vector2F::new(0., 5.)),
-                    );
-
                     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.line_height;
+                let mut current_y = hovered_point.y() + layout.position_map.line_height;
                 for hover_popover in hover_popovers {
                     let size = hover_popover.size();
                     let mut popover_origin = vec2f(hovered_point.x(), current_y);
@@ -713,11 +786,6 @@ impl EditorElement {
                         cx,
                     );
 
-                    paint.hover_popover_bounds.push(
-                        RectF::new(popover_origin, hover_popover.size())
-                            .dilate(Vector2F::new(0., 5.)),
-                    );
-
                     current_y = popover_origin.y() + size.y() + HOVER_POPOVER_GAP;
                 }
             }
@@ -753,14 +821,16 @@ impl EditorElement {
 
             let highlighted_range = HighlightedRange {
                 color,
-                line_height: layout.line_height,
+                line_height: layout.position_map.line_height,
                 corner_radius,
-                start_y: content_origin.y() + row_range.start as f32 * layout.line_height
+                start_y: content_origin.y()
+                    + row_range.start as f32 * layout.position_map.line_height
                     - scroll_top,
                 lines: row_range
                     .into_iter()
                     .map(|row| {
-                        let line_layout = &layout.line_layouts[(row - start_row) as usize];
+                        let line_layout =
+                            &layout.position_map.line_layouts[(row - start_row) as usize];
                         HighlightedRangeLine {
                             start_x: if row == range.start.row() {
                                 content_origin.x()
@@ -793,13 +863,16 @@ impl EditorElement {
         layout: &mut LayoutState,
         cx: &mut PaintContext,
     ) {
-        let scroll_position = layout.snapshot.scroll_position();
-        let scroll_left = scroll_position.x() * layout.em_width;
-        let scroll_top = scroll_position.y() * layout.line_height;
+        let scroll_position = layout.position_map.snapshot.scroll_position();
+        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 &mut layout.blocks {
-            let mut origin =
-                bounds.origin() + vec2f(0., block.row as f32 * layout.line_height - scroll_top);
+            let mut origin = bounds.origin()
+                + vec2f(
+                    0.,
+                    block.row as f32 * layout.position_map.line_height - scroll_top,
+                );
             if !matches!(block.style, BlockStyle::Sticky) {
                 origin += vec2f(-scroll_left, 0.);
             }
@@ -1048,7 +1121,7 @@ impl EditorElement {
 
                         enum JumpIcon {}
                         cx.render(&editor, |_, cx| {
-                            MouseEventHandler::new::<JumpIcon, _, _>(*key, cx, |state, _| {
+                            MouseEventHandler::<JumpIcon>::new(*key, cx, |state, _| {
                                 let style = style.jump_icon.style_for(state, false);
                                 Svg::new("icons/arrow_up_right_8.svg")
                                     .with_color(style.color)
@@ -1181,7 +1254,7 @@ impl EditorElement {
 
 impl Element for EditorElement {
     type LayoutState = LayoutState;
-    type PaintState = PaintState;
+    type PaintState = ();
 
     fn layout(
         &mut self,
@@ -1483,22 +1556,24 @@ impl Element for EditorElement {
         (
             size,
             LayoutState {
-                size,
-                scroll_max,
+                position_map: Arc::new(PositionMap {
+                    size,
+                    scroll_max,
+                    line_layouts,
+                    line_height,
+                    em_width,
+                    em_advance,
+                    snapshot,
+                }),
                 gutter_size,
                 gutter_padding,
                 text_size,
                 gutter_margin,
-                snapshot,
                 active_rows,
                 highlighted_rows,
                 highlighted_ranges,
-                line_layouts,
                 line_number_layouts,
                 blocks,
-                line_height,
-                em_width,
-                em_advance,
                 selections,
                 context_menu,
                 code_actions_indicator,
@@ -1522,19 +1597,21 @@ impl Element for EditorElement {
             layout.text_size,
         );
 
-        let mut paint_state = PaintState {
-            bounds,
-            gutter_bounds,
+        Self::attach_mouse_handlers(
+            &self.view,
+            &layout.position_map,
+            visible_bounds,
             text_bounds,
-            context_menu_bounds: None,
-            hover_popover_bounds: Default::default(),
-        };
+            gutter_bounds,
+            bounds,
+            cx,
+        );
 
         self.paint_background(gutter_bounds, text_bounds, layout, cx);
         if layout.gutter_size.x() > 0. {
             self.paint_gutter(gutter_bounds, visible_bounds, layout, cx);
         }
-        self.paint_text(text_bounds, visible_bounds, layout, &mut paint_state, cx);
+        self.paint_text(text_bounds, visible_bounds, layout, cx);
 
         if !layout.blocks.is_empty() {
             cx.scene.push_layer(Some(bounds));
@@ -1543,8 +1620,6 @@ impl Element for EditorElement {
         }
 
         cx.scene.pop_layer();
-
-        paint_state
     }
 
     fn dispatch_event(
@@ -1552,78 +1627,15 @@ impl Element for EditorElement {
         event: &Event,
         _: RectF,
         _: RectF,
-        layout: &mut LayoutState,
-        paint: &mut PaintState,
+        _: &mut LayoutState,
+        _: &mut (),
         cx: &mut EventContext,
     ) -> bool {
-        if let Some((_, context_menu)) = &mut layout.context_menu {
-            if context_menu.dispatch_event(event, cx) {
-                return true;
-            }
-        }
-
-        if let Some((_, indicator)) = &mut layout.code_actions_indicator {
-            if indicator.dispatch_event(event, cx) {
-                return true;
-            }
-        }
-
-        if let Some((_, popover_elements)) = &mut layout.hover_popovers {
-            for popover_element in popover_elements.iter_mut() {
-                if popover_element.dispatch_event(event, cx) {
-                    return true;
-                }
-            }
+        if let Event::ModifiersChanged(event) = event {
+            self.modifiers_changed(*event, cx);
         }
 
-        for block in &mut layout.blocks {
-            if block.element.dispatch_event(event, cx) {
-                return true;
-            }
-        }
-
-        match event {
-            &Event::MouseDown(
-                event @ MouseButtonEvent {
-                    button: MouseButton::Left,
-                    ..
-                },
-            ) => self.mouse_down(event, layout, paint, cx),
-
-            &Event::MouseDown(MouseButtonEvent {
-                button: MouseButton::Right,
-                position,
-                ..
-            }) => self.mouse_right_down(position, layout, paint, cx),
-
-            &Event::MouseUp(MouseButtonEvent {
-                button: MouseButton::Left,
-                position,
-                cmd,
-                shift,
-                ..
-            }) => self.mouse_up(position, cmd, shift, layout, paint, cx),
-
-            Event::MouseMoved(
-                event @ MouseMovedEvent {
-                    pressed_button: Some(MouseButton::Left),
-                    ..
-                },
-            ) => self.mouse_dragged(*event, layout, paint, cx),
-
-            Event::ScrollWheel(ScrollWheelEvent {
-                position,
-                delta,
-                precise,
-                ..
-            }) => self.scroll(*position, *delta, *precise, layout, paint, cx),
-
-            &Event::ModifiersChanged(event) => self.modifiers_changed(event, cx),
-
-            &Event::MouseMoved(event) => self.mouse_moved(event, layout, paint, cx),
-
-            _ => false,
-        }
+        false
     }
 
     fn rect_for_text_range(
@@ -1640,26 +1652,34 @@ impl Element for EditorElement {
             layout.text_size,
         );
         let content_origin = text_bounds.origin() + vec2f(layout.gutter_margin, 0.);
-        let scroll_position = layout.snapshot.scroll_position();
+        let scroll_position = layout.position_map.snapshot.scroll_position();
         let start_row = scroll_position.y() as u32;
-        let scroll_top = scroll_position.y() * layout.line_height;
-        let scroll_left = scroll_position.x() * layout.em_width;
+        let scroll_top = scroll_position.y() * layout.position_map.line_height;
+        let scroll_left = scroll_position.x() * layout.position_map.em_width;
 
-        let range_start =
-            OffsetUtf16(range_utf16.start).to_display_point(&layout.snapshot.display_snapshot);
+        let range_start = OffsetUtf16(range_utf16.start)
+            .to_display_point(&layout.position_map.snapshot.display_snapshot);
         if range_start.row() < start_row {
             return None;
         }
 
         let line = layout
+            .position_map
             .line_layouts
             .get((range_start.row() - start_row) as usize)?;
         let range_start_x = line.x_for_index(range_start.column() as usize);
-        let range_start_y = range_start.row() as f32 * layout.line_height;
+        let range_start_y = range_start.row() as f32 * layout.position_map.line_height;
         Some(RectF::new(
-            content_origin + vec2f(range_start_x, range_start_y + layout.line_height)
+            content_origin
+                + vec2f(
+                    range_start_x,
+                    range_start_y + layout.position_map.line_height,
+                )
                 - vec2f(scroll_left, scroll_top),
-            vec2f(layout.em_width, layout.line_height),
+            vec2f(
+                layout.position_map.em_width,
+                layout.position_map.line_height,
+            ),
         ))
     }
 
@@ -1678,21 +1698,15 @@ impl Element for EditorElement {
 }
 
 pub struct LayoutState {
-    size: Vector2F,
-    scroll_max: Vector2F,
+    position_map: Arc<PositionMap>,
     gutter_size: Vector2F,
     gutter_padding: f32,
     gutter_margin: f32,
     text_size: Vector2F,
-    snapshot: EditorSnapshot,
     active_rows: BTreeMap<u32, bool>,
     highlighted_rows: Option<Range<u32>>,
-    line_layouts: Vec<text_layout::Line>,
     line_number_layouts: Vec<Option<text_layout::Line>>,
     blocks: Vec<BlockLayout>,
-    line_height: f32,
-    em_width: f32,
-    em_advance: f32,
     highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
     selections: Vec<(ReplicaId, Vec<SelectionLayout>)>,
     context_menu: Option<(DisplayPoint, ElementBox)>,
@@ -1700,6 +1714,52 @@ pub struct LayoutState {
     hover_popovers: Option<(DisplayPoint, Vec<ElementBox>)>,
 }
 
+pub struct PositionMap {
+    size: Vector2F,
+    line_height: f32,
+    scroll_max: Vector2F,
+    em_width: f32,
+    em_advance: f32,
+    line_layouts: Vec<text_layout::Line>,
+    snapshot: EditorSnapshot,
+}
+
+impl PositionMap {
+    /// Returns two display points:
+    /// 1. The nearest *valid* position in the editor
+    /// 2. An unclipped, potentially *invalid* position that maps directly to
+    ///    the given pixel position.
+    fn point_for_position(
+        &self,
+        text_bounds: RectF,
+        position: Vector2F,
+    ) -> (DisplayPoint, DisplayPoint) {
+        let scroll_position = self.snapshot.scroll_position();
+        let position = position - text_bounds.origin();
+        let y = position.y().max(0.0).min(self.size.y());
+        let x = position.x() + (scroll_position.x() * self.em_width);
+        let row = (y / self.line_height + scroll_position.y()) as u32;
+        let (column, x_overshoot) = if let Some(line) = self
+            .line_layouts
+            .get(row as usize - scroll_position.y() as usize)
+        {
+            if let Some(ix) = line.index_for_x(x) {
+                (ix as u32, 0.0)
+            } else {
+                (line.len() as u32, 0f32.max(x - line.width()))
+            }
+        } else {
+            (0, x)
+        };
+
+        let mut target_point = DisplayPoint::new(row, column);
+        let point = self.snapshot.clip_point(target_point, Bias::Left);
+        *target_point.column_mut() += (x_overshoot / self.em_advance) as u32;
+
+        (point, target_point)
+    }
+}
+
 struct BlockLayout {
     row: u32,
     element: ElementBox,

crates/editor/src/hover_popover.rs 🔗

@@ -20,6 +20,10 @@ use crate::{
 pub const HOVER_DELAY_MILLIS: u64 = 350;
 pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200;
 
+pub const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.;
+pub const MIN_POPOVER_LINE_HEIGHT: f32 = 4.;
+pub const HOVER_POPOVER_GAP: f32 = 10.;
+
 #[derive(Clone, PartialEq)]
 pub struct HoverAt {
     pub point: Option<DisplayPoint>,
@@ -312,7 +316,7 @@ pub struct InfoPopover {
 
 impl InfoPopover {
     pub fn render(&self, style: &EditorStyle, cx: &mut RenderContext<Editor>) -> ElementBox {
-        MouseEventHandler::new::<InfoPopover, _, _>(0, cx, |_, cx| {
+        MouseEventHandler::<InfoPopover>::new(0, cx, |_, cx| {
             let mut flex = Flex::new(Axis::Vertical).scrollable::<HoverBlock, _>(1, None, cx);
             flex.extend(self.contents.iter().map(|content| {
                 let project = self.project.read(cx);
@@ -350,10 +354,11 @@ impl InfoPopover {
                 .with_style(style.hover_popover.container)
                 .boxed()
         })
+        .on_move(|_, _| {})
         .with_cursor_style(CursorStyle::Arrow)
         .with_padding(Padding {
-            bottom: 5.,
-            top: 5.,
+            bottom: HOVER_POPOVER_GAP,
+            top: HOVER_POPOVER_GAP,
             ..Default::default()
         })
         .boxed()
@@ -383,13 +388,19 @@ impl DiagnosticPopover {
 
         let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
 
-        MouseEventHandler::new::<DiagnosticPopover, _, _>(0, cx, |_, _| {
+        MouseEventHandler::<DiagnosticPopover>::new(0, cx, |_, _| {
             Text::new(self.local_diagnostic.diagnostic.message.clone(), text_style)
                 .with_soft_wrap(true)
                 .contained()
                 .with_style(container_style)
                 .boxed()
         })
+        .with_padding(Padding {
+            top: HOVER_POPOVER_GAP,
+            bottom: HOVER_POPOVER_GAP,
+            ..Default::default()
+        })
+        .on_move(|_, _| {})
         .on_click(MouseButton::Left, |_, cx| {
             cx.dispatch_action(GoToDiagnostic)
         })

crates/editor/src/mouse_context_menu.rs 🔗

@@ -1,5 +1,8 @@
 use context_menu::ContextMenuItem;
-use gpui::{geometry::vector::Vector2F, impl_internal_actions, MutableAppContext, ViewContext};
+use gpui::{
+    elements::AnchorCorner, geometry::vector::Vector2F, impl_internal_actions, MutableAppContext,
+    ViewContext,
+};
 
 use crate::{
     DisplayPoint, Editor, EditorMode, FindAllReferences, GoToDefinition, GoToTypeDefinition,
@@ -46,6 +49,7 @@ pub fn deploy_context_menu(
     editor.mouse_context_menu.update(cx, |menu, cx| {
         menu.show(
             position,
+            AnchorCorner::TopLeft,
             vec![
                 ContextMenuItem::item("Rename Symbol", Rename),
                 ContextMenuItem::item("Go To Definition", GoToDefinition),

crates/editor/src/test.rs 🔗

@@ -88,7 +88,7 @@ pub struct EditorTestContext<'a> {
 }
 
 impl<'a> EditorTestContext<'a> {
-    pub async fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> {
+    pub fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> {
         let (window_id, editor) = cx.update(|cx| {
             cx.set_global(Settings::test(cx));
             crate::init(cx);
@@ -364,7 +364,8 @@ impl<'a> EditorLspTestContext<'a> {
             .insert_tree("/root", json!({ "dir": { file_name: "" }}))
             .await;
 
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
+        let (window_id, workspace) =
+            cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
         project
             .update(cx, |project, cx| {
                 project.find_or_create_local_worktree("/root", true, cx)

crates/file_finder/src/file_finder.rs 🔗

@@ -316,7 +316,8 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+        let (window_id, workspace) =
+            cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
         cx.dispatch_action(window_id, Toggle);
 
         let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
@@ -370,7 +371,8 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+        let (_, workspace) =
+            cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
         let (_, finder) =
             cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
 
@@ -444,7 +446,8 @@ mod tests {
             cx,
         )
         .await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+        let (_, workspace) =
+            cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
         let (_, finder) =
             cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
         finder
@@ -468,7 +471,8 @@ mod tests {
             cx,
         )
         .await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+        let (_, workspace) =
+            cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
         let (_, finder) =
             cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
 
@@ -520,7 +524,8 @@ mod tests {
             cx,
         )
         .await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+        let (_, workspace) =
+            cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
         let (_, finder) =
             cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
 
@@ -558,7 +563,8 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+        let (_, workspace) =
+            cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
         let (_, finder) =
             cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
         finder

crates/gpui/Cargo.toml 🔗

@@ -3,6 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
 edition = "2021"
 name = "gpui"
 version = "0.1.0"
+description = "A GPU-accelerated UI framework"
 
 [lib]
 path = "src/gpui.rs"
@@ -59,6 +60,7 @@ png = "0.16"
 simplelog = "0.9"
 
 [target.'cfg(target_os = "macos")'.dependencies]
+media = { path = "../media" }
 anyhow = "1"
 block = "0.1"
 cocoa = "0.24"

crates/gpui/src/app.rs 🔗

@@ -9,8 +9,8 @@ use crate::{
     platform::{self, KeyDownEvent, Platform, PromptLevel, WindowOptions},
     presenter::Presenter,
     util::post_inc,
-    AssetCache, AssetSource, ClipboardItem, FontCache, InputHandler, MouseButton, MouseRegionId,
-    PathPromptOptions, TextLayoutCache,
+    Appearance, AssetCache, AssetSource, ClipboardItem, FontCache, InputHandler, MouseButton,
+    MouseRegionId, PathPromptOptions, TextLayoutCache,
 };
 pub use action::*;
 use anyhow::{anyhow, Context, Result};
@@ -579,6 +579,7 @@ impl TestAppContext {
                 hovered_region_ids: Default::default(),
                 clicked_region_ids: None,
                 refreshing: false,
+                appearance: Appearance::Light,
             };
             f(view, &mut render_cx)
         })
@@ -1243,6 +1244,10 @@ impl MutableAppContext {
             .map_or(false, |window| window.is_fullscreen)
     }
 
+    pub fn window_bounds(&self, window_id: usize) -> RectF {
+        self.presenters_and_platform_windows[&window_id].1.bounds()
+    }
+
     pub fn render_view(&mut self, params: RenderParams) -> Result<ElementBox> {
         let window_id = params.window_id;
         let view_id = params.view_id;
@@ -1260,6 +1265,7 @@ impl MutableAppContext {
         &mut self,
         window_id: usize,
         titlebar_height: f32,
+        appearance: Appearance,
     ) -> HashMap<usize, ElementBox> {
         self.start_frame();
         #[allow(clippy::needless_collect)]
@@ -1287,6 +1293,7 @@ impl MutableAppContext {
                         hovered_region_ids: Default::default(),
                         clicked_region_ids: None,
                         refreshing: false,
+                        appearance,
                     })
                     .unwrap(),
                 )
@@ -1920,46 +1927,61 @@ impl MutableAppContext {
                 },
             );
             root_view.update(this, |view, cx| view.on_focus_in(cx.handle().into(), cx));
-            this.open_platform_window(window_id, window_options);
+
+            let window =
+                this.cx
+                    .platform
+                    .open_window(window_id, window_options, this.foreground.clone());
+            this.register_platform_window(window_id, window);
 
             (window_id, root_view)
         })
     }
 
-    pub fn replace_root_view<T, F>(&mut self, window_id: usize, build_root_view: F) -> ViewHandle<T>
+    pub fn add_status_bar_item<T, F>(&mut self, build_root_view: F) -> (usize, ViewHandle<T>)
     where
         T: View,
         F: FnOnce(&mut ViewContext<T>) -> T,
     {
         self.update(|this| {
+            let window_id = post_inc(&mut this.next_window_id);
             let root_view = this
                 .build_and_insert_view(window_id, ParentId::Root, |cx| Some(build_root_view(cx)))
                 .unwrap();
-            let window = this.cx.windows.get_mut(&window_id).unwrap();
-            window.root_view = root_view.clone().into();
-            window.focused_view_id = Some(root_view.id());
-            root_view
-        })
-    }
+            this.cx.windows.insert(
+                window_id,
+                Window {
+                    root_view: root_view.clone().into(),
+                    focused_view_id: Some(root_view.id()),
+                    is_active: false,
+                    invalidation: None,
+                    is_fullscreen: false,
+                },
+            );
+            root_view.update(this, |view, cx| view.on_focus_in(cx.handle().into(), cx));
 
-    pub fn remove_window(&mut self, window_id: usize) {
-        self.cx.windows.remove(&window_id);
-        self.presenters_and_platform_windows.remove(&window_id);
-        self.flush_effects();
+            let status_item = this.cx.platform.add_status_item();
+            this.register_platform_window(window_id, status_item);
+
+            (window_id, root_view)
+        })
     }
 
-    fn open_platform_window(&mut self, window_id: usize, window_options: WindowOptions) {
-        let mut window =
-            self.cx
-                .platform
-                .open_window(window_id, window_options, self.foreground.clone());
-        let presenter = Rc::new(RefCell::new(
-            self.build_presenter(window_id, window.titlebar_height()),
-        ));
+    fn register_platform_window(
+        &mut self,
+        window_id: usize,
+        mut window: Box<dyn platform::Window>,
+    ) {
+        let presenter = Rc::new(RefCell::new(self.build_presenter(
+            window_id,
+            window.titlebar_height(),
+            window.appearance(),
+        )));
 
         {
             let mut app = self.upgrade();
             let presenter = Rc::downgrade(&presenter);
+
             window.on_event(Box::new(move |event| {
                 app.update(|cx| {
                     if let Some(presenter) = presenter.upgrade() {
@@ -2005,24 +2027,59 @@ impl MutableAppContext {
             }));
         }
 
+        {
+            let mut app = self.upgrade();
+            window.on_appearance_changed(Box::new(move || app.update(|cx| cx.refresh_windows())));
+        }
+
         window.set_input_handler(Box::new(WindowInputHandler {
             app: self.upgrade().0,
             window_id,
         }));
 
-        let scene =
-            presenter
-                .borrow_mut()
-                .build_scene(window.size(), window.scale_factor(), false, self);
+        let scene = presenter.borrow_mut().build_scene(
+            window.content_size(),
+            window.scale_factor(),
+            false,
+            self,
+        );
         window.present_scene(scene);
         self.presenters_and_platform_windows
             .insert(window_id, (presenter.clone(), window));
     }
 
-    pub fn build_presenter(&mut self, window_id: usize, titlebar_height: f32) -> Presenter {
+    pub fn replace_root_view<T, F>(&mut self, window_id: usize, build_root_view: F) -> ViewHandle<T>
+    where
+        T: View,
+        F: FnOnce(&mut ViewContext<T>) -> T,
+    {
+        self.update(|this| {
+            let root_view = this
+                .build_and_insert_view(window_id, ParentId::Root, |cx| Some(build_root_view(cx)))
+                .unwrap();
+            let window = this.cx.windows.get_mut(&window_id).unwrap();
+            window.root_view = root_view.clone().into();
+            window.focused_view_id = Some(root_view.id());
+            root_view
+        })
+    }
+
+    pub fn remove_window(&mut self, window_id: usize) {
+        self.cx.windows.remove(&window_id);
+        self.presenters_and_platform_windows.remove(&window_id);
+        self.flush_effects();
+    }
+
+    pub fn build_presenter(
+        &mut self,
+        window_id: usize,
+        titlebar_height: f32,
+        appearance: Appearance,
+    ) -> Presenter {
         Presenter::new(
             window_id,
             titlebar_height,
+            appearance,
             self.cx.font_cache.clone(),
             TextLayoutCache::new(self.cx.platform.fonts()),
             self.assets.clone(),
@@ -2360,9 +2417,13 @@ impl MutableAppContext {
             {
                 {
                     let mut presenter = presenter.borrow_mut();
-                    presenter.invalidate(&mut invalidation, self);
-                    let scene =
-                        presenter.build_scene(window.size(), window.scale_factor(), false, self);
+                    presenter.invalidate(&mut invalidation, window.appearance(), self);
+                    let scene = presenter.build_scene(
+                        window.content_size(),
+                        window.scale_factor(),
+                        false,
+                        self,
+                    );
                     window.present_scene(scene);
                 }
                 self.presenters_and_platform_windows
@@ -2424,9 +2485,11 @@ impl MutableAppContext {
             let mut presenter = presenter.borrow_mut();
             presenter.refresh(
                 invalidation.as_mut().unwrap_or(&mut Default::default()),
+                window.appearance(),
                 self,
             );
-            let scene = presenter.build_scene(window.size(), window.scale_factor(), true, self);
+            let scene =
+                presenter.build_scene(window.content_size(), window.scale_factor(), true, self);
             window.present_scene(scene);
         }
         self.presenters_and_platform_windows = presenters;
@@ -3698,6 +3761,10 @@ impl<'a, T: View> ViewContext<'a, T> {
         self.app.toggle_window_full_screen(self.window_id)
     }
 
+    pub fn window_bounds(&self) -> RectF {
+        self.app.window_bounds(self.window_id)
+    }
+
     pub fn prompt(
         &self,
         level: PromptLevel,
@@ -4028,8 +4095,9 @@ pub struct RenderParams {
     pub view_id: usize,
     pub titlebar_height: f32,
     pub hovered_region_ids: HashSet<MouseRegionId>,
-    pub clicked_region_ids: Option<(Vec<MouseRegionId>, MouseButton)>,
+    pub clicked_region_ids: Option<(HashSet<MouseRegionId>, MouseButton)>,
     pub refreshing: bool,
+    pub appearance: Appearance,
 }
 
 pub struct RenderContext<'a, T: View> {
@@ -4037,9 +4105,10 @@ pub struct RenderContext<'a, T: View> {
     pub(crate) view_id: usize,
     pub(crate) view_type: PhantomData<T>,
     pub(crate) hovered_region_ids: HashSet<MouseRegionId>,
-    pub(crate) clicked_region_ids: Option<(Vec<MouseRegionId>, MouseButton)>,
+    pub(crate) clicked_region_ids: Option<(HashSet<MouseRegionId>, MouseButton)>,
     pub app: &'a mut MutableAppContext,
     pub titlebar_height: f32,
+    pub appearance: Appearance,
     pub refreshing: bool,
 }
 
@@ -4060,6 +4129,7 @@ impl<'a, V: View> RenderContext<'a, V> {
             hovered_region_ids: params.hovered_region_ids.clone(),
             clicked_region_ids: params.clicked_region_ids.clone(),
             refreshing: params.refreshing,
+            appearance: params.appearance,
         }
     }
 
@@ -4076,10 +4146,7 @@ impl<'a, V: View> RenderContext<'a, V> {
     }
 
     pub fn mouse_state<Tag: 'static>(&self, region_id: usize) -> MouseState {
-        let region_id = MouseRegionId {
-            view_id: self.view_id,
-            discriminant: (TypeId::of::<Tag>(), region_id),
-        };
+        let region_id = MouseRegionId::new::<Tag>(self.view_id, region_id);
         MouseState {
             hovered: self.hovered_region_ids.contains(&region_id),
             clicked: self.clicked_region_ids.as_ref().and_then(|(ids, button)| {
@@ -4092,9 +4159,10 @@ impl<'a, V: View> RenderContext<'a, V> {
         }
     }
 
-    pub fn element_state<Tag: 'static, T: 'static + Default>(
+    pub fn element_state<Tag: 'static, T: 'static>(
         &mut self,
         element_id: usize,
+        initial: T,
     ) -> ElementStateHandle<T> {
         let id = ElementStateId {
             view_id: self.view_id(),
@@ -4104,9 +4172,16 @@ impl<'a, V: View> RenderContext<'a, V> {
         self.cx
             .element_states
             .entry(id)
-            .or_insert_with(|| Box::new(T::default()));
+            .or_insert_with(|| Box::new(initial));
         ElementStateHandle::new(id, self.frame_count, &self.cx.ref_counts)
     }
+
+    pub fn default_element_state<Tag: 'static, T: 'static + Default>(
+        &mut self,
+        element_id: usize,
+    ) -> ElementStateHandle<T> {
+        self.element_state::<Tag, T>(element_id, T::default())
+    }
 }
 
 impl AsRef<AppContext> for &AppContext {
@@ -5229,6 +5304,10 @@ impl<T: 'static> ElementStateHandle<T> {
         }
     }
 
+    pub fn id(&self) -> ElementStateId {
+        self.id
+    }
+
     pub fn read<'a>(&self, cx: &'a AppContext) -> &'a T {
         cx.element_states
             .get(&self.id)
@@ -6032,12 +6111,12 @@ mod tests {
         }
 
         impl super::View for View {
-            fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
+            fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
+                enum Handler {}
                 let mouse_down_count = self.mouse_down_count.clone();
-                EventHandler::new(Empty::new().boxed())
-                    .on_mouse_down(move |_| {
+                MouseEventHandler::<Handler>::new(0, cx, |_, _| Empty::new().boxed())
+                    .on_down(MouseButton::Left, move |_, _| {
                         mouse_down_count.fetch_add(1, SeqCst);
-                        true
                     })
                     .boxed()
             }

crates/gpui/src/elements.rs 🔗

@@ -3,7 +3,6 @@ mod canvas;
 mod constrained_box;
 mod container;
 mod empty;
-mod event_handler;
 mod expanded;
 mod flex;
 mod hook;
@@ -13,6 +12,7 @@ mod label;
 mod list;
 mod mouse_event_handler;
 mod overlay;
+mod resizable;
 mod stack;
 mod svg;
 mod text;
@@ -21,8 +21,8 @@ mod uniform_list;
 
 use self::expanded::Expanded;
 pub use self::{
-    align::*, canvas::*, constrained_box::*, container::*, empty::*, event_handler::*, flex::*,
-    hook::*, image::*, keystroke_label::*, label::*, list::*, mouse_event_handler::*, overlay::*,
+    align::*, canvas::*, constrained_box::*, container::*, empty::*, flex::*, hook::*, image::*,
+    keystroke_label::*, label::*, list::*, mouse_event_handler::*, overlay::*, resizable::*,
     stack::*, svg::*, text::*, tooltip::*, uniform_list::*,
 };
 pub use crate::presenter::ChildView;
@@ -187,6 +187,27 @@ pub trait Element {
     {
         Tooltip::new::<Tag, T>(id, text, action, style, self.boxed(), cx)
     }
+
+    fn with_resize_handle<Tag: 'static, T: View>(
+        self,
+        element_id: usize,
+        side: Side,
+        handle_size: f32,
+        initial_size: f32,
+        cx: &mut RenderContext<T>,
+    ) -> Resizable
+    where
+        Self: 'static + Sized,
+    {
+        Resizable::new::<Tag, T>(
+            self.boxed(),
+            element_id,
+            side,
+            handle_size,
+            initial_size,
+            cx,
+        )
+    }
 }
 
 pub enum Lifecycle<T: Element> {

crates/gpui/src/elements/container.rs 🔗

@@ -373,6 +373,24 @@ pub struct Padding {
     pub right: f32,
 }
 
+impl Padding {
+    pub fn horizontal(padding: f32) -> Self {
+        Self {
+            left: padding,
+            right: padding,
+            ..Default::default()
+        }
+    }
+
+    pub fn vertical(padding: f32) -> Self {
+        Self {
+            top: padding,
+            bottom: padding,
+            ..Default::default()
+        }
+    }
+}
+
 impl<'de> Deserialize<'de> for Padding {
     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
     where

crates/gpui/src/elements/event_handler.rs 🔗

@@ -1,177 +0,0 @@
-use crate::{
-    geometry::vector::Vector2F, presenter::MeasurementContext, CursorRegion, DebugContext, Element,
-    ElementBox, Event, EventContext, LayoutContext, MouseButton, MouseButtonEvent, MouseRegion,
-    NavigationDirection, PaintContext, SizeConstraint,
-};
-use pathfinder_geometry::rect::RectF;
-use serde_json::json;
-use std::{any::TypeId, ops::Range};
-
-pub struct EventHandler {
-    child: ElementBox,
-    capture_all: Option<(TypeId, usize)>,
-    mouse_down: Option<Box<dyn FnMut(&mut EventContext) -> bool>>,
-    right_mouse_down: Option<Box<dyn FnMut(&mut EventContext) -> bool>>,
-    navigate_mouse_down: Option<Box<dyn FnMut(NavigationDirection, &mut EventContext) -> bool>>,
-}
-
-impl EventHandler {
-    pub fn new(child: ElementBox) -> Self {
-        Self {
-            child,
-            capture_all: None,
-            mouse_down: None,
-            right_mouse_down: None,
-            navigate_mouse_down: None,
-        }
-    }
-
-    pub fn on_mouse_down<F>(mut self, callback: F) -> Self
-    where
-        F: 'static + FnMut(&mut EventContext) -> bool,
-    {
-        self.mouse_down = Some(Box::new(callback));
-        self
-    }
-
-    pub fn on_right_mouse_down<F>(mut self, callback: F) -> Self
-    where
-        F: 'static + FnMut(&mut EventContext) -> bool,
-    {
-        self.right_mouse_down = Some(Box::new(callback));
-        self
-    }
-
-    pub fn on_navigate_mouse_down<F>(mut self, callback: F) -> Self
-    where
-        F: 'static + FnMut(NavigationDirection, &mut EventContext) -> bool,
-    {
-        self.navigate_mouse_down = Some(Box::new(callback));
-        self
-    }
-
-    pub fn capture_all<T: 'static>(mut self, id: usize) -> Self {
-        self.capture_all = Some((TypeId::of::<T>(), id));
-        self
-    }
-}
-
-impl Element for EventHandler {
-    type LayoutState = ();
-    type PaintState = ();
-
-    fn layout(
-        &mut self,
-        constraint: SizeConstraint,
-        cx: &mut LayoutContext,
-    ) -> (Vector2F, Self::LayoutState) {
-        let size = self.child.layout(constraint, cx);
-        (size, ())
-    }
-
-    fn paint(
-        &mut self,
-        bounds: RectF,
-        visible_bounds: RectF,
-        _: &mut Self::LayoutState,
-        cx: &mut PaintContext,
-    ) -> Self::PaintState {
-        if let Some(discriminant) = self.capture_all {
-            cx.scene.push_stacking_context(None);
-            cx.scene.push_cursor_region(CursorRegion {
-                bounds: visible_bounds,
-                style: Default::default(),
-            });
-            cx.scene.push_mouse_region(MouseRegion::handle_all(
-                cx.current_view_id(),
-                Some(discriminant),
-                visible_bounds,
-            ));
-            cx.scene.pop_stacking_context();
-        }
-        self.child.paint(bounds.origin(), visible_bounds, cx);
-    }
-
-    fn dispatch_event(
-        &mut self,
-        event: &Event,
-        _: RectF,
-        visible_bounds: RectF,
-        _: &mut Self::LayoutState,
-        _: &mut Self::PaintState,
-        cx: &mut EventContext,
-    ) -> bool {
-        if self.capture_all.is_some() {
-            return true;
-        }
-
-        if self.child.dispatch_event(event, cx) {
-            true
-        } else {
-            match event {
-                Event::MouseDown(MouseButtonEvent {
-                    button: MouseButton::Left,
-                    position,
-                    ..
-                }) => {
-                    if let Some(callback) = self.mouse_down.as_mut() {
-                        if visible_bounds.contains_point(*position) {
-                            return callback(cx);
-                        }
-                    }
-                    false
-                }
-                Event::MouseDown(MouseButtonEvent {
-                    button: MouseButton::Right,
-                    position,
-                    ..
-                }) => {
-                    if let Some(callback) = self.right_mouse_down.as_mut() {
-                        if visible_bounds.contains_point(*position) {
-                            return callback(cx);
-                        }
-                    }
-                    false
-                }
-                Event::MouseDown(MouseButtonEvent {
-                    button: MouseButton::Navigate(direction),
-                    position,
-                    ..
-                }) => {
-                    if let Some(callback) = self.navigate_mouse_down.as_mut() {
-                        if visible_bounds.contains_point(*position) {
-                            return callback(*direction, cx);
-                        }
-                    }
-                    false
-                }
-                _ => false,
-            }
-        }
-    }
-
-    fn rect_for_text_range(
-        &self,
-        range_utf16: Range<usize>,
-        _: RectF,
-        _: RectF,
-        _: &Self::LayoutState,
-        _: &Self::PaintState,
-        cx: &MeasurementContext,
-    ) -> Option<RectF> {
-        self.child.rect_for_text_range(range_utf16, cx)
-    }
-
-    fn debug(
-        &self,
-        _: RectF,
-        _: &Self::LayoutState,
-        _: &Self::PaintState,
-        cx: &DebugContext,
-    ) -> serde_json::Value {
-        json!({
-            "type": "EventHandler",
-            "child": self.child.debug(cx),
-        })
-    }
-}

crates/gpui/src/elements/flex.rs 🔗

@@ -1,11 +1,10 @@
-use std::{any::Any, f32::INFINITY, ops::Range};
+use std::{any::Any, cell::Cell, f32::INFINITY, ops::Range, rc::Rc};
 
 use crate::{
     json::{self, ToJson, Value},
     presenter::MeasurementContext,
     Axis, DebugContext, Element, ElementBox, ElementStateHandle, Event, EventContext,
-    LayoutContext, MouseMovedEvent, PaintContext, RenderContext, ScrollWheelEvent, SizeConstraint,
-    Vector2FExt, View,
+    LayoutContext, PaintContext, RenderContext, SizeConstraint, Vector2FExt, View,
 };
 use pathfinder_geometry::{
     rect::RectF,
@@ -15,14 +14,14 @@ use serde_json::json;
 
 #[derive(Default)]
 struct ScrollState {
-    scroll_to: Option<usize>,
-    scroll_position: f32,
+    scroll_to: Cell<Option<usize>>,
+    scroll_position: Cell<f32>,
 }
 
 pub struct Flex {
     axis: Axis,
     children: Vec<ElementBox>,
-    scroll_state: Option<ElementStateHandle<ScrollState>>,
+    scroll_state: Option<(ElementStateHandle<Rc<ScrollState>>, usize)>,
 }
 
 impl Flex {
@@ -52,9 +51,9 @@ impl Flex {
         Tag: 'static,
         V: View,
     {
-        let scroll_state = cx.element_state::<Tag, ScrollState>(element_id);
-        scroll_state.update(cx, |scroll_state, _| scroll_state.scroll_to = scroll_to);
-        self.scroll_state = Some(scroll_state);
+        let scroll_state = cx.default_element_state::<Tag, Rc<ScrollState>>(element_id);
+        scroll_state.read(cx).scroll_to.set(scroll_to);
+        self.scroll_state = Some((scroll_state, cx.handle().id()));
         self
     }
 
@@ -202,9 +201,9 @@ impl Element for Flex {
         }
 
         if let Some(scroll_state) = self.scroll_state.as_ref() {
-            scroll_state.update(cx, |scroll_state, _| {
+            scroll_state.0.update(cx, |scroll_state, _| {
                 if let Some(scroll_to) = scroll_state.scroll_to.take() {
-                    let visible_start = scroll_state.scroll_position;
+                    let visible_start = scroll_state.scroll_position.get();
                     let visible_end = visible_start + size.along(self.axis);
                     if let Some(child) = self.children.get(scroll_to) {
                         let child_start: f32 = self.children[..scroll_to]
@@ -213,15 +212,22 @@ impl Element for Flex {
                             .sum();
                         let child_end = child_start + child.size().along(self.axis);
                         if child_start < visible_start {
-                            scroll_state.scroll_position = child_start;
+                            scroll_state.scroll_position.set(child_start);
                         } else if child_end > visible_end {
-                            scroll_state.scroll_position = child_end - size.along(self.axis);
+                            scroll_state
+                                .scroll_position
+                                .set(child_end - size.along(self.axis));
                         }
                     }
                 }
 
-                scroll_state.scroll_position =
-                    scroll_state.scroll_position.min(-remaining_space).max(0.);
+                scroll_state.scroll_position.set(
+                    scroll_state
+                        .scroll_position
+                        .get()
+                        .min(-remaining_space)
+                        .max(0.),
+                );
             });
         }
 
@@ -242,9 +248,45 @@ impl Element for Flex {
             cx.scene.push_layer(Some(bounds));
         }
 
+        if let Some(scroll_state) = &self.scroll_state {
+            cx.scene.push_mouse_region(
+                crate::MouseRegion::new::<Self>(scroll_state.1, 0, bounds)
+                    .on_scroll({
+                        let scroll_state = scroll_state.0.read(cx).clone();
+                        let axis = self.axis;
+                        move |e, cx| {
+                            if remaining_space < 0. {
+                                let mut delta = match axis {
+                                    Axis::Horizontal => {
+                                        if e.delta.x() != 0. {
+                                            e.delta.x()
+                                        } else {
+                                            e.delta.y()
+                                        }
+                                    }
+                                    Axis::Vertical => e.delta.y(),
+                                };
+                                if !e.precise {
+                                    delta *= 20.;
+                                }
+
+                                scroll_state
+                                    .scroll_position
+                                    .set(scroll_state.scroll_position.get() - delta);
+
+                                cx.notify();
+                            } else {
+                                cx.propogate_event();
+                            }
+                        }
+                    })
+                    .on_move(|_, _| { /* Capture move events */ }),
+            )
+        }
+
         let mut child_origin = bounds.origin();
         if let Some(scroll_state) = self.scroll_state.as_ref() {
-            let scroll_position = scroll_state.read(cx).scroll_position;
+            let scroll_position = scroll_state.0.read(cx).scroll_position.get();
             match self.axis {
                 Axis::Horizontal => child_origin.set_x(child_origin.x() - scroll_position),
                 Axis::Vertical => child_origin.set_y(child_origin.y() - scroll_position),
@@ -278,9 +320,9 @@ impl Element for Flex {
     fn dispatch_event(
         &mut self,
         event: &Event,
-        bounds: RectF,
         _: RectF,
-        remaining_space: &mut Self::LayoutState,
+        _: RectF,
+        _: &mut Self::LayoutState,
         _: &mut Self::PaintState,
         cx: &mut EventContext,
     ) -> bool {
@@ -288,50 +330,6 @@ impl Element for Flex {
         for child in &mut self.children {
             handled = child.dispatch_event(event, cx) || handled;
         }
-        if !handled {
-            if let &Event::ScrollWheel(ScrollWheelEvent {
-                position,
-                delta,
-                precise,
-                ..
-            }) = event
-            {
-                if *remaining_space < 0. && bounds.contains_point(position) {
-                    if let Some(scroll_state) = self.scroll_state.as_ref() {
-                        scroll_state.update(cx, |scroll_state, cx| {
-                            let mut delta = match self.axis {
-                                Axis::Horizontal => {
-                                    if delta.x() != 0. {
-                                        delta.x()
-                                    } else {
-                                        delta.y()
-                                    }
-                                }
-                                Axis::Vertical => delta.y(),
-                            };
-                            if !precise {
-                                delta *= 20.;
-                            }
-
-                            scroll_state.scroll_position -= delta;
-
-                            handled = true;
-                            cx.notify();
-                        });
-                    }
-                }
-            }
-        }
-
-        if !handled {
-            if let &Event::MouseMoved(MouseMovedEvent { position, .. }) = event {
-                // If this is a scrollable flex, and the mouse is over it, eat the scroll event to prevent
-                // propogating it to the element below.
-                if self.scroll_state.is_some() && bounds.contains_point(position) {
-                    handled = true;
-                }
-            }
-        }
 
         handled
     }

crates/gpui/src/elements/list.rs 🔗

@@ -5,8 +5,8 @@ use crate::{
     },
     json::json,
     presenter::MeasurementContext,
-    DebugContext, Element, ElementBox, ElementRc, Event, EventContext, LayoutContext, PaintContext,
-    RenderContext, ScrollWheelEvent, SizeConstraint, View, ViewContext,
+    DebugContext, Element, ElementBox, ElementRc, Event, EventContext, LayoutContext, MouseRegion,
+    PaintContext, RenderContext, SizeConstraint, View, ViewContext,
 };
 use std::{cell::RefCell, collections::VecDeque, ops::Range, rc::Rc};
 use sum_tree::{Bias, SumTree};
@@ -263,6 +263,22 @@ impl Element for List {
     ) {
         cx.scene.push_layer(Some(bounds));
 
+        cx.scene
+            .push_mouse_region(MouseRegion::new::<Self>(10, 0, bounds).on_scroll({
+                let state = self.state.clone();
+                let height = bounds.height();
+                let scroll_top = scroll_top.clone();
+                move |e, cx| {
+                    state.0.borrow_mut().scroll(
+                        &scroll_top,
+                        height,
+                        e.platform_event.delta,
+                        e.platform_event.precise,
+                        cx,
+                    )
+                }
+            }));
+
         let state = &mut *self.state.0.borrow_mut();
         for (mut element, origin) in state.visible_elements(bounds, scroll_top) {
             element.paint(origin, visible_bounds, cx);
@@ -312,20 +328,6 @@ impl Element for List {
         drop(cursor);
         state.items = new_items;
 
-        if let Event::ScrollWheel(ScrollWheelEvent {
-            position,
-            delta,
-            precise,
-            ..
-        }) = event
-        {
-            if bounds.contains_point(*position)
-                && state.scroll(scroll_top, bounds.height(), *delta, *precise, cx)
-            {
-                handled = true;
-            }
-        }
-
         handled
     }
 
@@ -527,7 +529,7 @@ impl StateInner {
         mut delta: Vector2F,
         precise: bool,
         cx: &mut EventContext,
-    ) -> bool {
+    ) {
         if !precise {
             delta *= 20.;
         }
@@ -554,9 +556,6 @@ impl StateInner {
             let visible_range = self.visible_range(height, scroll_top);
             self.scroll_handler.as_mut().unwrap()(visible_range, cx);
         }
-        cx.notify();
-
-        true
     }
 
     fn scroll_top(&self, logical_scroll_top: &ListOffset) -> f32 {
@@ -659,7 +658,7 @@ mod tests {
 
     #[crate::test(self)]
     fn test_layout(cx: &mut crate::MutableAppContext) {
-        let mut presenter = cx.build_presenter(0, 0.);
+        let mut presenter = cx.build_presenter(0, 0., Default::default());
         let (_, view) = cx.add_window(Default::default(), |_| TestView);
         let constraint = SizeConstraint::new(vec2f(0., 0.), vec2f(100., 40.));
 
@@ -759,7 +758,7 @@ mod tests {
             .unwrap_or(10);
 
         let (_, view) = cx.add_window(Default::default(), |_| TestView);
-        let mut presenter = cx.build_presenter(0, 0.);
+        let mut presenter = cx.build_presenter(0, 0., Default::default());
         let mut next_id = 0;
         let elements = Rc::new(RefCell::new(
             (0..rng.gen_range(0..=20))

crates/gpui/src/elements/mouse_event_handler.rs 🔗

@@ -7,37 +7,39 @@ use crate::{
     platform::CursorStyle,
     scene::{
         ClickRegionEvent, CursorRegion, DownOutRegionEvent, DownRegionEvent, DragRegionEvent,
-        HandlerSet, HoverRegionEvent, MoveRegionEvent, UpOutRegionEvent, UpRegionEvent,
+        HandlerSet, HoverRegionEvent, MoveRegionEvent, ScrollWheelRegionEvent, UpOutRegionEvent,
+        UpRegionEvent,
     },
     DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, MeasurementContext,
     MouseButton, MouseRegion, MouseState, PaintContext, RenderContext, SizeConstraint, View,
 };
 use serde_json::json;
-use std::{any::TypeId, ops::Range};
+use std::{marker::PhantomData, ops::Range};
 
-pub struct MouseEventHandler {
+pub struct MouseEventHandler<Tag: 'static> {
     child: ElementBox,
-    discriminant: (TypeId, usize),
+    region_id: usize,
     cursor_style: Option<CursorStyle>,
     handlers: HandlerSet,
     hoverable: bool,
     padding: Padding,
+    _tag: PhantomData<Tag>,
 }
 
-impl MouseEventHandler {
-    pub fn new<Tag, V, F>(id: usize, cx: &mut RenderContext<V>, render_child: F) -> Self
+impl<Tag> MouseEventHandler<Tag> {
+    pub fn new<V, F>(region_id: usize, cx: &mut RenderContext<V>, render_child: F) -> Self
     where
-        Tag: 'static,
         V: View,
         F: FnOnce(MouseState, &mut RenderContext<V>) -> ElementBox,
     {
         Self {
-            child: render_child(cx.mouse_state::<Tag>(id), cx),
+            child: render_child(cx.mouse_state::<Tag>(region_id), cx),
+            region_id,
             cursor_style: None,
-            discriminant: (TypeId::of::<Tag>(), id),
             handlers: Default::default(),
             hoverable: true,
             padding: Default::default(),
+            _tag: PhantomData,
         }
     }
 
@@ -121,6 +123,14 @@ impl MouseEventHandler {
         self
     }
 
+    pub fn on_scroll(
+        mut self,
+        handler: impl Fn(ScrollWheelRegionEvent, &mut EventContext) + 'static,
+    ) -> Self {
+        self.handlers = self.handlers.on_scroll(handler);
+        self
+    }
+
     pub fn with_hoverable(mut self, is_hoverable: bool) -> Self {
         self.hoverable = is_hoverable;
         self
@@ -140,7 +150,7 @@ impl MouseEventHandler {
     }
 }
 
-impl Element for MouseEventHandler {
+impl<Tag> Element for MouseEventHandler<Tag> {
     type LayoutState = ();
     type PaintState = ();
 
@@ -168,9 +178,9 @@ impl Element for MouseEventHandler {
         }
 
         cx.scene.push_mouse_region(
-            MouseRegion::from_handlers(
+            MouseRegion::from_handlers::<Tag>(
                 cx.current_view_id(),
-                Some(self.discriminant),
+                self.region_id,
                 hit_bounds,
                 self.handlers.clone(),
             )

crates/gpui/src/elements/overlay.rs 🔗

@@ -4,14 +4,15 @@ use crate::{
     geometry::{rect::RectF, vector::Vector2F},
     json::ToJson,
     presenter::MeasurementContext,
-    DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, MouseRegion,
+    Axis, DebugContext, Element, ElementBox, Event, EventContext, LayoutContext, MouseRegion,
     PaintContext, SizeConstraint,
 };
 use serde_json::json;
 
 pub struct Overlay {
     child: ElementBox,
-    abs_position: Option<Vector2F>,
+    anchor_position: Option<Vector2F>,
+    anchor_corner: AnchorCorner,
     fit_mode: OverlayFitMode,
     hoverable: bool,
 }
@@ -19,31 +20,79 @@ pub struct Overlay {
 #[derive(Copy, Clone)]
 pub enum OverlayFitMode {
     SnapToWindow,
-    FlipAlignment,
+    SwitchAnchor,
     None,
 }
 
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub enum AnchorCorner {
+    TopLeft,
+    TopRight,
+    BottomLeft,
+    BottomRight,
+}
+
+impl AnchorCorner {
+    fn get_bounds(&self, anchor_position: Vector2F, size: Vector2F) -> RectF {
+        match self {
+            Self::TopLeft => RectF::from_points(anchor_position, anchor_position + size),
+            Self::TopRight => RectF::from_points(
+                anchor_position - Vector2F::new(size.x(), 0.),
+                anchor_position + Vector2F::new(0., size.y()),
+            ),
+            Self::BottomLeft => RectF::from_points(
+                anchor_position - Vector2F::new(0., size.y()),
+                anchor_position + Vector2F::new(size.x(), 0.),
+            ),
+            Self::BottomRight => RectF::from_points(anchor_position - size, anchor_position),
+        }
+    }
+
+    fn switch_axis(self, axis: Axis) -> Self {
+        match axis {
+            Axis::Vertical => match self {
+                AnchorCorner::TopLeft => AnchorCorner::BottomLeft,
+                AnchorCorner::TopRight => AnchorCorner::BottomRight,
+                AnchorCorner::BottomLeft => AnchorCorner::TopLeft,
+                AnchorCorner::BottomRight => AnchorCorner::TopRight,
+            },
+            Axis::Horizontal => match self {
+                AnchorCorner::TopLeft => AnchorCorner::TopRight,
+                AnchorCorner::TopRight => AnchorCorner::TopLeft,
+                AnchorCorner::BottomLeft => AnchorCorner::BottomRight,
+                AnchorCorner::BottomRight => AnchorCorner::BottomLeft,
+            },
+        }
+    }
+}
+
 impl Overlay {
     pub fn new(child: ElementBox) -> Self {
         Self {
             child,
-            abs_position: None,
+            anchor_position: None,
+            anchor_corner: AnchorCorner::TopLeft,
             fit_mode: OverlayFitMode::None,
             hoverable: false,
         }
     }
 
-    pub fn with_abs_position(mut self, position: Vector2F) -> Self {
-        self.abs_position = Some(position);
+    pub fn with_anchor_position(mut self, position: Vector2F) -> Self {
+        self.anchor_position = Some(position);
         self
     }
 
-    pub fn fit_mode(mut self, fit_mode: OverlayFitMode) -> Self {
+    pub fn with_anchor_corner(mut self, anchor_corner: AnchorCorner) -> Self {
+        self.anchor_corner = anchor_corner;
+        self
+    }
+
+    pub fn with_fit_mode(mut self, fit_mode: OverlayFitMode) -> Self {
         self.fit_mode = fit_mode;
         self
     }
 
-    pub fn hoverable(mut self, hoverable: bool) -> Self {
+    pub fn with_hoverable(mut self, hoverable: bool) -> Self {
         self.hoverable = hoverable;
         self
     }
@@ -58,7 +107,7 @@ impl Element for Overlay {
         constraint: SizeConstraint,
         cx: &mut LayoutContext,
     ) -> (Vector2F, Self::LayoutState) {
-        let constraint = if self.abs_position.is_some() {
+        let constraint = if self.anchor_position.is_some() {
             SizeConstraint::new(Vector2F::zero(), cx.window_size)
         } else {
             constraint
@@ -74,45 +123,75 @@ impl Element for Overlay {
         size: &mut Self::LayoutState,
         cx: &mut PaintContext,
     ) {
-        let mut bounds = RectF::new(self.abs_position.unwrap_or_else(|| bounds.origin()), *size);
-        cx.scene.push_stacking_context(None);
-
-        if self.hoverable {
-            cx.scene.push_mouse_region(MouseRegion {
-                view_id: cx.current_view_id(),
-                bounds,
-                ..Default::default()
-            });
-        }
+        let anchor_position = self.anchor_position.unwrap_or_else(|| bounds.origin());
+        let mut bounds = self.anchor_corner.get_bounds(anchor_position, *size);
 
         match self.fit_mode {
             OverlayFitMode::SnapToWindow => {
-                // Snap the right edge of the overlay to the right edge of the window if
-                // its horizontal bounds overflow.
-                if bounds.lower_right().x() > cx.window_size.x() {
-                    bounds.set_origin_x((cx.window_size.x() - bounds.width()).max(0.));
+                // Snap the horizontal edges of the overlay to the horizontal edges of the window if
+                // its horizontal bounds overflow
+                if bounds.max_x() > cx.window_size.x() {
+                    let mut lower_right = bounds.lower_right();
+                    lower_right.set_x(cx.window_size.x());
+                    bounds = RectF::from_points(lower_right - *size, lower_right);
+                } else if bounds.min_x() < 0. {
+                    let mut upper_left = bounds.origin();
+                    upper_left.set_x(0.);
+                    bounds = RectF::from_points(upper_left, upper_left + *size);
                 }
 
-                // Snap the bottom edge of the overlay to the bottom edge of the window if
+                // Snap the vertical edges of the overlay to the vertical edges of the window if
                 // its vertical bounds overflow.
-                if bounds.lower_right().y() > cx.window_size.y() {
-                    bounds.set_origin_y((cx.window_size.y() - bounds.height()).max(0.));
+                if bounds.max_y() > cx.window_size.y() {
+                    let mut lower_right = bounds.lower_right();
+                    lower_right.set_y(cx.window_size.y());
+                    bounds = RectF::from_points(lower_right - *size, lower_right);
+                } else if bounds.min_y() < 0. {
+                    let mut upper_left = bounds.origin();
+                    upper_left.set_y(0.);
+                    bounds = RectF::from_points(upper_left, upper_left + *size);
                 }
             }
-            OverlayFitMode::FlipAlignment => {
-                // Right-align overlay if its horizontal bounds overflow.
-                if bounds.lower_right().x() > cx.window_size.x() {
-                    bounds.set_origin_x(bounds.origin_x() - bounds.width());
+            OverlayFitMode::SwitchAnchor => {
+                let mut anchor_corner = self.anchor_corner;
+
+                if bounds.max_x() > cx.window_size.x() {
+                    anchor_corner = anchor_corner.switch_axis(Axis::Horizontal);
+                }
+
+                if bounds.max_y() > cx.window_size.y() {
+                    anchor_corner = anchor_corner.switch_axis(Axis::Vertical);
+                }
+
+                if bounds.min_x() < 0. {
+                    anchor_corner = anchor_corner.switch_axis(Axis::Horizontal)
                 }
 
-                // Bottom-align overlay if its vertical bounds overflow.
-                if bounds.lower_right().y() > cx.window_size.y() {
-                    bounds.set_origin_y(bounds.origin_y() - bounds.height());
+                if bounds.min_y() < 0. {
+                    anchor_corner = anchor_corner.switch_axis(Axis::Vertical)
+                }
+
+                // Update bounds if needed
+                if anchor_corner != self.anchor_corner {
+                    bounds = anchor_corner.get_bounds(anchor_position, *size)
                 }
             }
             OverlayFitMode::None => {}
         }
 
+        cx.scene.push_stacking_context(None);
+
+        if self.hoverable {
+            enum OverlayHoverCapture {}
+            // Block hovers in lower stacking contexts
+            cx.scene
+                .push_mouse_region(MouseRegion::new::<OverlayHoverCapture>(
+                    cx.current_view_id(),
+                    cx.current_view_id(),
+                    bounds,
+                ));
+        }
+
         self.child.paint(bounds.origin(), bounds, cx);
         cx.scene.pop_stacking_context();
     }
@@ -150,7 +229,7 @@ impl Element for Overlay {
     ) -> serde_json::Value {
         json!({
             "type": "Overlay",
-            "abs_position": self.abs_position.to_json(),
+            "abs_position": self.anchor_position.to_json(),
             "child": self.child.debug(cx),
         })
     }

crates/gpui/src/elements/resizable.rs 🔗

@@ -0,0 +1,225 @@
+use std::{cell::Cell, rc::Rc};
+
+use pathfinder_geometry::vector::{vec2f, Vector2F};
+use serde_json::json;
+
+use crate::{
+    geometry::rect::RectF, scene::DragRegionEvent, Axis, CursorStyle, Element, ElementBox,
+    ElementStateHandle, MouseButton, MouseRegion, RenderContext, View,
+};
+
+use super::{ConstrainedBox, Hook};
+
+#[derive(Copy, Clone, Debug)]
+pub enum Side {
+    Top,
+    Bottom,
+    Left,
+    Right,
+}
+
+impl Side {
+    fn axis(&self) -> Axis {
+        match self {
+            Side::Left | Side::Right => Axis::Horizontal,
+            Side::Top | Side::Bottom => Axis::Vertical,
+        }
+    }
+
+    /// 'before' is in reference to the standard english document ordering of left-to-right
+    /// then top-to-bottom
+    fn before_content(self) -> bool {
+        match self {
+            Side::Left | Side::Top => true,
+            Side::Right | Side::Bottom => false,
+        }
+    }
+
+    fn relevant_component(&self, vector: Vector2F) -> f32 {
+        match self.axis() {
+            Axis::Horizontal => vector.x(),
+            Axis::Vertical => vector.y(),
+        }
+    }
+
+    fn compute_delta(&self, e: DragRegionEvent) -> f32 {
+        if self.before_content() {
+            self.relevant_component(e.prev_mouse_position) - self.relevant_component(e.position)
+        } else {
+            self.relevant_component(e.position) - self.relevant_component(e.prev_mouse_position)
+        }
+    }
+
+    fn of_rect(&self, bounds: RectF, handle_size: f32) -> RectF {
+        match self {
+            Side::Top => RectF::new(bounds.origin(), vec2f(bounds.width(), handle_size)),
+            Side::Left => RectF::new(bounds.origin(), vec2f(handle_size, bounds.height())),
+            Side::Bottom => {
+                let mut origin = bounds.lower_left();
+                origin.set_y(origin.y() - handle_size);
+                RectF::new(origin, vec2f(bounds.width(), handle_size))
+            }
+            Side::Right => {
+                let mut origin = bounds.upper_right();
+                origin.set_x(origin.x() - handle_size);
+                RectF::new(origin, vec2f(handle_size, bounds.height()))
+            }
+        }
+    }
+}
+
+struct ResizeHandleState {
+    actual_dimension: Cell<f32>,
+    custom_dimension: Cell<f32>,
+}
+
+pub struct Resizable {
+    side: Side,
+    handle_size: f32,
+    child: ElementBox,
+    state: Rc<ResizeHandleState>,
+    _state_handle: ElementStateHandle<Rc<ResizeHandleState>>,
+}
+
+impl Resizable {
+    pub fn new<Tag: 'static, T: View>(
+        child: ElementBox,
+        element_id: usize,
+        side: Side,
+        handle_size: f32,
+        initial_size: f32,
+        cx: &mut RenderContext<T>,
+    ) -> Self {
+        let state_handle = cx.element_state::<Tag, Rc<ResizeHandleState>>(
+            element_id,
+            Rc::new(ResizeHandleState {
+                actual_dimension: Cell::new(initial_size),
+                custom_dimension: Cell::new(initial_size),
+            }),
+        );
+
+        let state = state_handle.read(cx).clone();
+
+        let child = Hook::new({
+            let constrained = ConstrainedBox::new(child);
+            match side.axis() {
+                Axis::Horizontal => constrained.with_max_width(state.custom_dimension.get()),
+                Axis::Vertical => constrained.with_max_height(state.custom_dimension.get()),
+            }
+            .boxed()
+        })
+        .on_after_layout({
+            let state = state.clone();
+            move |size, _| {
+                state.actual_dimension.set(side.relevant_component(size));
+            }
+        })
+        .boxed();
+
+        Self {
+            side,
+            child,
+            handle_size,
+            state,
+            _state_handle: state_handle,
+        }
+    }
+
+    pub fn current_size(&self) -> f32 {
+        self.state.actual_dimension.get()
+    }
+}
+
+impl Element for Resizable {
+    type LayoutState = ();
+    type PaintState = ();
+
+    fn layout(
+        &mut self,
+        constraint: crate::SizeConstraint,
+        cx: &mut crate::LayoutContext,
+    ) -> (Vector2F, Self::LayoutState) {
+        (self.child.layout(constraint, cx), ())
+    }
+
+    fn paint(
+        &mut self,
+        bounds: pathfinder_geometry::rect::RectF,
+        visible_bounds: pathfinder_geometry::rect::RectF,
+        _child_size: &mut Self::LayoutState,
+        cx: &mut crate::PaintContext,
+    ) -> Self::PaintState {
+        cx.scene.push_stacking_context(None);
+
+        let handle_region = self.side.of_rect(bounds, self.handle_size);
+
+        enum ResizeHandle {}
+        cx.scene.push_mouse_region(
+            MouseRegion::new::<ResizeHandle>(
+                cx.current_view_id(),
+                self.side as usize,
+                handle_region,
+            )
+            .on_down(MouseButton::Left, |_, _| {}) // This prevents the mouse down event from being propagated elsewhere
+            .on_drag(MouseButton::Left, {
+                let state = self.state.clone();
+                let side = self.side;
+                move |e, cx| {
+                    let prev_width = state.actual_dimension.get();
+                    state
+                        .custom_dimension
+                        .set(0f32.max(prev_width + side.compute_delta(e)).round());
+                    cx.notify();
+                }
+            }),
+        );
+
+        cx.scene.push_cursor_region(crate::CursorRegion {
+            bounds: handle_region,
+            style: match self.side.axis() {
+                Axis::Horizontal => CursorStyle::ResizeLeftRight,
+                Axis::Vertical => CursorStyle::ResizeUpDown,
+            },
+        });
+
+        cx.scene.pop_stacking_context();
+
+        self.child.paint(bounds.origin(), visible_bounds, cx);
+    }
+
+    fn dispatch_event(
+        &mut self,
+        event: &crate::Event,
+        _bounds: pathfinder_geometry::rect::RectF,
+        _visible_bounds: pathfinder_geometry::rect::RectF,
+        _layout: &mut Self::LayoutState,
+        _paint: &mut Self::PaintState,
+        cx: &mut crate::EventContext,
+    ) -> bool {
+        self.child.dispatch_event(event, cx)
+    }
+
+    fn rect_for_text_range(
+        &self,
+        range_utf16: std::ops::Range<usize>,
+        _bounds: pathfinder_geometry::rect::RectF,
+        _visible_bounds: pathfinder_geometry::rect::RectF,
+        _layout: &Self::LayoutState,
+        _paint: &Self::PaintState,
+        cx: &crate::MeasurementContext,
+    ) -> Option<pathfinder_geometry::rect::RectF> {
+        self.child.rect_for_text_range(range_utf16, cx)
+    }
+
+    fn debug(
+        &self,
+        _bounds: pathfinder_geometry::rect::RectF,
+        _layout: &Self::LayoutState,
+        _paint: &Self::PaintState,
+        cx: &crate::DebugContext,
+    ) -> serde_json::Value {
+        json!({
+            "child": self.child.debug(cx),
+        })
+    }
+}

crates/gpui/src/elements/text.rs 🔗

@@ -291,7 +291,7 @@ mod tests {
     #[crate::test(self)]
     fn test_soft_wrapping_with_carriage_returns(cx: &mut MutableAppContext) {
         let (window_id, _) = cx.add_window(Default::default(), |_| TestView);
-        let mut presenter = cx.build_presenter(window_id, Default::default());
+        let mut presenter = cx.build_presenter(window_id, Default::default(), Default::default());
         fonts::with_font_cache(cx.font_cache().clone(), || {
             let mut text = Text::new("Hello\r\n".into(), Default::default()).with_soft_wrap(true);
             let (_, state) = text.layout(

crates/gpui/src/elements/tooltip.rs 🔗

@@ -62,7 +62,7 @@ impl Tooltip {
         struct ElementState<Tag>(Tag);
         struct MouseEventHandlerState<Tag>(Tag);
 
-        let state_handle = cx.element_state::<ElementState<Tag>, Rc<TooltipState>>(id);
+        let state_handle = cx.default_element_state::<ElementState<Tag>, Rc<TooltipState>>(id);
         let state = state_handle.read(cx).clone();
         let tooltip = if state.visible.get() {
             let mut collapsed_tooltip = Self::render_tooltip(
@@ -84,42 +84,41 @@ impl Tooltip {
                         })
                         .boxed(),
                 )
-                .fit_mode(OverlayFitMode::FlipAlignment)
-                .with_abs_position(state.position.get())
+                .with_fit_mode(OverlayFitMode::SwitchAnchor)
+                .with_anchor_position(state.position.get())
                 .boxed(),
             )
         } else {
             None
         };
-        let child =
-            MouseEventHandler::new::<MouseEventHandlerState<Tag>, _, _>(id, cx, |_, _| child)
-                .on_hover(move |e, cx| {
-                    let position = e.position;
-                    let window_id = cx.window_id();
-                    if let Some(view_id) = cx.view_id() {
-                        if e.started {
-                            if !state.visible.get() {
-                                state.position.set(position);
+        let child = MouseEventHandler::<MouseEventHandlerState<Tag>>::new(id, cx, |_, _| child)
+            .on_hover(move |e, cx| {
+                let position = e.position;
+                let window_id = cx.window_id();
+                if let Some(view_id) = cx.view_id() {
+                    if e.started {
+                        if !state.visible.get() {
+                            state.position.set(position);
 
-                                let mut debounce = state.debounce.borrow_mut();
-                                if debounce.is_none() {
-                                    *debounce = Some(cx.spawn({
-                                        let state = state.clone();
-                                        |mut cx| async move {
-                                            cx.background().timer(DEBOUNCE_TIMEOUT).await;
-                                            state.visible.set(true);
-                                            cx.update(|cx| cx.notify_view(window_id, view_id));
-                                        }
-                                    }));
-                                }
+                            let mut debounce = state.debounce.borrow_mut();
+                            if debounce.is_none() {
+                                *debounce = Some(cx.spawn({
+                                    let state = state.clone();
+                                    |mut cx| async move {
+                                        cx.background().timer(DEBOUNCE_TIMEOUT).await;
+                                        state.visible.set(true);
+                                        cx.update(|cx| cx.notify_view(window_id, view_id));
+                                    }
+                                }));
                             }
-                        } else {
-                            state.visible.set(false);
-                            state.debounce.take();
                         }
+                    } else {
+                        state.visible.set(false);
+                        state.debounce.take();
                     }
-                })
-                .boxed();
+                }
+            })
+            .boxed();
         Self {
             child,
             tooltip,

crates/gpui/src/elements/uniform_list.rs 🔗

@@ -6,7 +6,8 @@ use crate::{
     },
     json::{self, json},
     presenter::MeasurementContext,
-    ElementBox, RenderContext, ScrollWheelEvent, View,
+    scene::ScrollWheelRegionEvent,
+    ElementBox, MouseRegion, RenderContext, ScrollWheelEvent, View,
 };
 use json::ToJson;
 use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
@@ -50,6 +51,7 @@ pub struct UniformList {
     padding_top: f32,
     padding_bottom: f32,
     get_width_from_item: Option<usize>,
+    view_id: usize,
 }
 
 impl UniformList {
@@ -77,6 +79,7 @@ impl UniformList {
             padding_top: 0.,
             padding_bottom: 0.,
             get_width_from_item: None,
+            view_id: cx.handle().id(),
         }
     }
 
@@ -96,7 +99,7 @@ impl UniformList {
     }
 
     fn scroll(
-        &self,
+        state: UniformListState,
         _: Vector2F,
         mut delta: Vector2F,
         precise: bool,
@@ -107,7 +110,7 @@ impl UniformList {
             delta *= 20.;
         }
 
-        let mut state = self.state.0.borrow_mut();
+        let mut state = state.0.borrow_mut();
         state.scroll_top = (state.scroll_top - delta.y()).max(0.0).min(scroll_max);
         cx.notify();
 
@@ -283,6 +286,28 @@ impl Element for UniformList {
     ) -> Self::PaintState {
         cx.scene.push_layer(Some(bounds));
 
+        cx.scene.push_mouse_region(
+            MouseRegion::new::<Self>(self.view_id, 0, visible_bounds).on_scroll({
+                let scroll_max = layout.scroll_max;
+                let state = self.state.clone();
+                move |ScrollWheelRegionEvent {
+                          platform_event:
+                              ScrollWheelEvent {
+                                  position,
+                                  delta,
+                                  precise,
+                                  ..
+                              },
+                          ..
+                      },
+                      cx| {
+                    if !Self::scroll(state.clone(), position, delta, precise, scroll_max, cx) {
+                        cx.propogate_event();
+                    }
+                }
+            }),
+        );
+
         let mut item_origin = bounds.origin()
             - vec2f(
                 0.,
@@ -300,7 +325,7 @@ impl Element for UniformList {
     fn dispatch_event(
         &mut self,
         event: &Event,
-        bounds: RectF,
+        _: RectF,
         _: RectF,
         layout: &mut Self::LayoutState,
         _: &mut Self::PaintState,
@@ -311,20 +336,6 @@ impl Element for UniformList {
             handled = item.dispatch_event(event, cx) || handled;
         }
 
-        if let Event::ScrollWheel(ScrollWheelEvent {
-            position,
-            delta,
-            precise,
-            ..
-        }) = event
-        {
-            if bounds.contains_point(*position)
-                && self.scroll(*position, *delta, *precise, layout.scroll_max, cx)
-            {
-                handled = true;
-            }
-        }
-
         handled
     }
 

crates/gpui/src/platform.rs 🔗

@@ -39,6 +39,11 @@ pub trait Platform: Send + Sync {
     fn fonts(&self) -> Arc<dyn FontSystem>;
 
     fn activate(&self, ignoring_other_apps: bool);
+    fn hide(&self);
+    fn hide_other_apps(&self);
+    fn unhide_other_apps(&self);
+    fn quit(&self);
+
     fn open_window(
         &self,
         id: usize,
@@ -46,10 +51,8 @@ pub trait Platform: Send + Sync {
         executor: Rc<executor::Foreground>,
     ) -> Box<dyn Window>;
     fn key_window_id(&self) -> Option<usize>;
-    fn hide(&self);
-    fn hide_other_apps(&self);
-    fn unhide_other_apps(&self);
-    fn quit(&self);
+
+    fn add_status_item(&self) -> Box<dyn Window>;
 
     fn write_to_clipboard(&self, item: ClipboardItem);
     fn read_from_clipboard(&self) -> Option<ClipboardItem>;
@@ -107,7 +110,7 @@ pub trait InputHandler {
     fn rect_for_range(&self, range_utf16: Range<usize>) -> Option<RectF>;
 }
 
-pub trait Window: WindowContext {
+pub trait Window {
     fn as_any_mut(&mut self) -> &mut dyn Any;
     fn on_event(&mut self, callback: Box<dyn FnMut(Event) -> bool>);
     fn on_active_status_change(&mut self, callback: Box<dyn FnMut(bool)>);
@@ -124,23 +127,52 @@ pub trait Window: WindowContext {
     fn minimize(&self);
     fn zoom(&self);
     fn toggle_full_screen(&self);
-}
 
-pub trait WindowContext {
-    fn size(&self) -> Vector2F;
+    fn bounds(&self) -> RectF;
+    fn content_size(&self) -> Vector2F;
     fn scale_factor(&self) -> f32;
     fn titlebar_height(&self) -> f32;
     fn present_scene(&mut self, scene: Scene);
+    fn appearance(&self) -> Appearance;
+    fn on_appearance_changed(&mut self, callback: Box<dyn FnMut()>);
 }
 
 #[derive(Debug)]
 pub struct WindowOptions<'a> {
     pub bounds: WindowBounds,
+    pub titlebar: Option<TitlebarOptions<'a>>,
+    pub center: bool,
+    pub kind: WindowKind,
+    pub is_movable: bool,
+}
+
+#[derive(Debug)]
+pub struct TitlebarOptions<'a> {
     pub title: Option<&'a str>,
-    pub titlebar_appears_transparent: bool,
+    pub appears_transparent: bool,
     pub traffic_light_position: Option<Vector2F>,
 }
 
+#[derive(Copy, Clone, Debug)]
+pub enum Appearance {
+    Light,
+    VibrantLight,
+    Dark,
+    VibrantDark,
+}
+
+impl Default for Appearance {
+    fn default() -> Self {
+        Self::Light
+    }
+}
+
+#[derive(Copy, Clone, Debug)]
+pub enum WindowKind {
+    Normal,
+    PopUp,
+}
+
 #[derive(Debug)]
 pub enum WindowBounds {
     Maximized,
@@ -163,10 +195,17 @@ pub enum PromptLevel {
 pub enum CursorStyle {
     Arrow,
     ResizeLeftRight,
+    ResizeUpDown,
     PointingHand,
     IBeam,
 }
 
+impl Default for CursorStyle {
+    fn default() -> Self {
+        Self::Arrow
+    }
+}
+
 #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
 pub struct AppVersion {
     major: usize,
@@ -174,12 +213,6 @@ pub struct AppVersion {
     patch: usize,
 }
 
-impl Default for CursorStyle {
-    fn default() -> Self {
-        Self::Arrow
-    }
-}
-
 impl FromStr for AppVersion {
     type Err = anyhow::Error;
 
@@ -246,9 +279,14 @@ impl<'a> Default for WindowOptions<'a> {
     fn default() -> Self {
         Self {
             bounds: WindowBounds::Maximized,
-            title: Default::default(),
-            titlebar_appears_transparent: Default::default(),
-            traffic_light_position: Default::default(),
+            titlebar: Some(TitlebarOptions {
+                title: Default::default(),
+                appears_transparent: Default::default(),
+                traffic_light_position: Default::default(),
+            }),
+            center: false,
+            kind: WindowKind::Normal,
+            is_movable: true,
         }
     }
 }

crates/gpui/src/platform/mac.rs 🔗

@@ -1,3 +1,4 @@
+mod appearance;
 mod atlas;
 mod dispatcher;
 mod event;
@@ -7,12 +8,14 @@ mod image_cache;
 mod platform;
 mod renderer;
 mod sprite_cache;
+mod status_item;
 mod window;
 
 use cocoa::base::{BOOL, NO, YES};
 pub use dispatcher::Dispatcher;
 pub use fonts::FontSystem;
 use platform::{MacForegroundPlatform, MacPlatform};
+pub use renderer::Surface;
 use std::{rc::Rc, sync::Arc};
 use window::Window;
 

crates/gpui/src/platform/mac/appearance.rs 🔗

@@ -0,0 +1,37 @@
+use std::ffi::CStr;
+
+use cocoa::{
+    appkit::{NSAppearanceNameVibrantDark, NSAppearanceNameVibrantLight},
+    base::id,
+    foundation::NSString,
+};
+use objc::{msg_send, sel, sel_impl};
+
+use crate::Appearance;
+
+impl Appearance {
+    pub unsafe fn from_native(appearance: id) -> Self {
+        let name: id = msg_send![appearance, name];
+        if name == NSAppearanceNameVibrantLight {
+            Self::VibrantLight
+        } else if name == NSAppearanceNameVibrantDark {
+            Self::VibrantDark
+        } else if name == NSAppearanceNameAqua {
+            Self::Light
+        } else if name == NSAppearanceNameDarkAqua {
+            Self::Dark
+        } else {
+            println!(
+                "unknown appearance: {:?}",
+                CStr::from_ptr(name.UTF8String())
+            );
+            Self::Light
+        }
+    }
+}
+
+#[link(name = "AppKit", kind = "framework")]
+extern "C" {
+    pub static NSAppearanceNameAqua: id;
+    pub static NSAppearanceNameDarkAqua: id;
+}

crates/gpui/src/platform/mac/platform.rs 🔗

@@ -1,4 +1,6 @@
-use super::{event::key_to_native, BoolExt as _, Dispatcher, FontSystem, Window};
+use super::{
+    event::key_to_native, status_item::StatusItem, BoolExt as _, Dispatcher, FontSystem, Window,
+};
 use crate::{
     executor, keymap,
     platform::{self, CursorStyle},
@@ -50,6 +52,9 @@ use time::UtcOffset;
 #[allow(non_upper_case_globals)]
 const NSUTF8StringEncoding: NSUInteger = 4;
 
+#[allow(non_upper_case_globals)]
+pub const NSViewLayerContentsRedrawDuringViewResize: NSInteger = 2;
+
 const MAC_PLATFORM_IVAR: &str = "platform";
 static mut APP_CLASS: *const Class = ptr::null();
 static mut APP_DELEGATE_CLASS: *const Class = ptr::null();
@@ -439,23 +444,6 @@ impl platform::Platform for MacPlatform {
         }
     }
 
-    fn open_window(
-        &self,
-        id: usize,
-        options: platform::WindowOptions,
-        executor: Rc<executor::Foreground>,
-    ) -> Box<dyn platform::Window> {
-        Box::new(Window::open(id, options, executor, self.fonts()))
-    }
-
-    fn key_window_id(&self) -> Option<usize> {
-        Window::key_window_id()
-    }
-
-    fn fonts(&self) -> Arc<dyn platform::FontSystem> {
-        self.fonts.clone()
-    }
-
     fn hide(&self) {
         unsafe {
             let app = NSApplication::sharedApplication(nil);
@@ -497,6 +485,27 @@ impl platform::Platform for MacPlatform {
         }
     }
 
+    fn open_window(
+        &self,
+        id: usize,
+        options: platform::WindowOptions,
+        executor: Rc<executor::Foreground>,
+    ) -> Box<dyn platform::Window> {
+        Box::new(Window::open(id, options, executor, self.fonts()))
+    }
+
+    fn key_window_id(&self) -> Option<usize> {
+        Window::key_window_id()
+    }
+
+    fn add_status_item(&self) -> Box<dyn platform::Window> {
+        Box::new(StatusItem::add(self.fonts()))
+    }
+
+    fn fonts(&self) -> Arc<dyn platform::FontSystem> {
+        self.fonts.clone()
+    }
+
     fn write_to_clipboard(&self, item: ClipboardItem) {
         unsafe {
             self.pasteboard.clearContents();
@@ -681,6 +690,7 @@ impl platform::Platform for MacPlatform {
             let cursor: id = match style {
                 CursorStyle::Arrow => msg_send![class!(NSCursor), arrowCursor],
                 CursorStyle::ResizeLeftRight => msg_send![class!(NSCursor), resizeLeftRightCursor],
+                CursorStyle::ResizeUpDown => msg_send![class!(NSCursor), resizeUpDownCursor],
                 CursorStyle::PointingHand => msg_send![class!(NSCursor), pointingHandCursor],
                 CursorStyle::IBeam => msg_send![class!(NSCursor), IBeamCursor],
             };

crates/gpui/src/platform/mac/renderer.rs 🔗

@@ -8,16 +8,26 @@ use crate::{
     platform,
     scene::{Glyph, Icon, Image, ImageGlyph, Layer, Quad, Scene, Shadow, Underline},
 };
-use cocoa::foundation::NSUInteger;
+use cocoa::{
+    base::{NO, YES},
+    foundation::NSUInteger,
+    quartzcore::AutoresizingMask,
+};
+use core_foundation::base::TCFType;
+use foreign_types::ForeignTypeRef;
 use log::warn;
-use metal::{MTLPixelFormat, MTLResourceOptions, NSRange};
+use media::core_video::{self, CVMetalTextureCache};
+use metal::{CommandQueue, MTLPixelFormat, MTLResourceOptions, NSRange};
+use objc::{self, msg_send, sel, sel_impl};
 use shaders::ToFloat2 as _;
-use std::{collections::HashMap, ffi::c_void, iter::Peekable, mem, sync::Arc, vec};
+use std::{collections::HashMap, ffi::c_void, iter::Peekable, mem, ptr, sync::Arc, vec};
 
 const SHADERS_METALLIB: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/shaders.metallib"));
 const INSTANCE_BUFFER_SIZE: usize = 8192 * 1024; // This is an arbitrary decision. There's probably a more optimal value.
 
 pub struct Renderer {
+    layer: metal::MetalLayer,
+    command_queue: CommandQueue,
     sprite_cache: SpriteCache,
     image_cache: ImageCache,
     path_atlases: AtlasAllocator,
@@ -25,10 +35,12 @@ pub struct Renderer {
     shadow_pipeline_state: metal::RenderPipelineState,
     sprite_pipeline_state: metal::RenderPipelineState,
     image_pipeline_state: metal::RenderPipelineState,
+    surface_pipeline_state: metal::RenderPipelineState,
     path_atlas_pipeline_state: metal::RenderPipelineState,
     underline_pipeline_state: metal::RenderPipelineState,
     unit_vertices: metal::Buffer,
     instances: metal::Buffer,
+    cv_texture_cache: core_video::CVMetalTextureCache,
 }
 
 struct PathSprite {
@@ -37,13 +49,37 @@ struct PathSprite {
     shader_data: shaders::GPUISprite,
 }
 
+pub struct Surface {
+    pub bounds: RectF,
+    pub image_buffer: core_video::CVImageBuffer,
+}
+
 impl Renderer {
-    pub fn new(
-        device: metal::Device,
-        pixel_format: metal::MTLPixelFormat,
-        scale_factor: f32,
-        fonts: Arc<dyn platform::FontSystem>,
-    ) -> Self {
+    pub fn new(is_opaque: bool, fonts: Arc<dyn platform::FontSystem>) -> Self {
+        const PIXEL_FORMAT: MTLPixelFormat = MTLPixelFormat::BGRA8Unorm;
+
+        let device: metal::Device = if let Some(device) = metal::Device::system_default() {
+            device
+        } else {
+            log::error!("unable to access a compatible graphics device");
+            std::process::exit(1);
+        };
+
+        let layer = metal::MetalLayer::new();
+        layer.set_device(&device);
+        layer.set_pixel_format(PIXEL_FORMAT);
+        layer.set_presents_with_transaction(true);
+        layer.set_opaque(is_opaque);
+        unsafe {
+            let _: () = msg_send![&*layer, setAllowsNextDrawableTimeout: NO];
+            let _: () = msg_send![&*layer, setNeedsDisplayOnBoundsChange: YES];
+            let _: () = msg_send![
+                &*layer,
+                setAutoresizingMask: AutoresizingMask::WIDTH_SIZABLE
+                    | AutoresizingMask::HEIGHT_SIZABLE
+            ];
+        }
+
         let library = device
             .new_library_with_data(SHADERS_METALLIB)
             .expect("error building metal library");
@@ -66,13 +102,8 @@ impl Renderer {
             MTLResourceOptions::StorageModeManaged,
         );
 
-        let sprite_cache = SpriteCache::new(
-            device.clone(),
-            vec2i(1024, 768),
-            scale_factor,
-            fonts.clone(),
-        );
-        let image_cache = ImageCache::new(device.clone(), vec2i(1024, 768), scale_factor, fonts);
+        let sprite_cache = SpriteCache::new(device.clone(), vec2i(1024, 768), 1., fonts.clone());
+        let image_cache = ImageCache::new(device.clone(), vec2i(1024, 768), 1., fonts);
         let path_atlases =
             AtlasAllocator::new(device.clone(), build_path_atlas_texture_descriptor());
         let quad_pipeline_state = build_pipeline_state(
@@ -81,7 +112,7 @@ impl Renderer {
             "quad",
             "quad_vertex",
             "quad_fragment",
-            pixel_format,
+            PIXEL_FORMAT,
         );
         let shadow_pipeline_state = build_pipeline_state(
             &device,
@@ -89,7 +120,7 @@ impl Renderer {
             "shadow",
             "shadow_vertex",
             "shadow_fragment",
-            pixel_format,
+            PIXEL_FORMAT,
         );
         let sprite_pipeline_state = build_pipeline_state(
             &device,
@@ -97,7 +128,7 @@ impl Renderer {
             "sprite",
             "sprite_vertex",
             "sprite_fragment",
-            pixel_format,
+            PIXEL_FORMAT,
         );
         let image_pipeline_state = build_pipeline_state(
             &device,
@@ -105,7 +136,15 @@ impl Renderer {
             "image",
             "image_vertex",
             "image_fragment",
-            pixel_format,
+            PIXEL_FORMAT,
+        );
+        let surface_pipeline_state = build_pipeline_state(
+            &device,
+            &library,
+            "surface",
+            "surface_vertex",
+            "surface_fragment",
+            PIXEL_FORMAT,
         );
         let path_atlas_pipeline_state = build_path_atlas_pipeline_state(
             &device,
@@ -121,9 +160,12 @@ impl Renderer {
             "underline",
             "underline_vertex",
             "underline_fragment",
-            pixel_format,
+            PIXEL_FORMAT,
         );
+        let cv_texture_cache = CVMetalTextureCache::new(device.as_ptr()).unwrap();
         Self {
+            layer,
+            command_queue: device.new_command_queue(),
             sprite_cache,
             image_cache,
             path_atlases,
@@ -131,20 +173,26 @@ impl Renderer {
             shadow_pipeline_state,
             sprite_pipeline_state,
             image_pipeline_state,
+            surface_pipeline_state,
             path_atlas_pipeline_state,
             underline_pipeline_state,
             unit_vertices,
             instances,
+            cv_texture_cache,
         }
     }
 
-    pub fn render(
-        &mut self,
-        scene: &Scene,
-        drawable_size: Vector2F,
-        command_buffer: &metal::CommandBufferRef,
-        output: &metal::TextureRef,
-    ) {
+    pub fn layer(&self) -> &metal::MetalLayerRef {
+        &*self.layer
+    }
+
+    pub fn render(&mut self, scene: &Scene) {
+        let layer = self.layer.clone();
+        let drawable_size = layer.drawable_size();
+        let drawable = layer.next_drawable().unwrap();
+        let command_queue = self.command_queue.clone();
+        let command_buffer = command_queue.new_command_buffer();
+
         self.sprite_cache.set_scale_factor(scene.scale_factor());
         self.image_cache.set_scale_factor(scene.scale_factor());
 
@@ -155,15 +203,19 @@ impl Renderer {
             scene,
             path_sprites,
             &mut offset,
-            drawable_size,
+            vec2f(drawable_size.width as f32, drawable_size.height as f32),
             command_buffer,
-            output,
+            drawable.texture(),
         );
         self.instances.did_modify_range(NSRange {
             location: 0,
             length: offset as NSUInteger,
         });
         self.image_cache.finish_frame();
+
+        command_buffer.commit();
+        command_buffer.wait_until_completed();
+        drawable.present();
     }
 
     fn render_path_atlases(
@@ -312,7 +364,8 @@ impl Renderer {
         color_attachment.set_texture(Some(output));
         color_attachment.set_load_action(metal::MTLLoadAction::Clear);
         color_attachment.set_store_action(metal::MTLStoreAction::Store);
-        color_attachment.set_clear_color(metal::MTLClearColor::new(0., 0., 0., 1.));
+        let alpha = if self.layer.is_opaque() { 1. } else { 0. };
+        color_attachment.set_clear_color(metal::MTLClearColor::new(0., 0., 0., alpha));
         let command_encoder = command_buffer.new_render_command_encoder(render_pass_descriptor);
 
         command_encoder.set_viewport(metal::MTLViewport {
@@ -372,6 +425,13 @@ impl Renderer {
                 drawable_size,
                 command_encoder,
             );
+            self.render_surfaces(
+                layer.surfaces(),
+                scale_factor,
+                offset,
+                drawable_size,
+                command_encoder,
+            );
         }
 
         command_encoder.end_encoding();
@@ -768,6 +828,111 @@ impl Renderer {
         }
     }
 
+    fn render_surfaces(
+        &mut self,
+        surfaces: &[Surface],
+        scale_factor: f32,
+        offset: &mut usize,
+        drawable_size: Vector2F,
+        command_encoder: &metal::RenderCommandEncoderRef,
+    ) {
+        if surfaces.is_empty() {
+            return;
+        }
+
+        command_encoder.set_render_pipeline_state(&self.surface_pipeline_state);
+        command_encoder.set_vertex_buffer(
+            shaders::GPUISurfaceVertexInputIndex_GPUISurfaceVertexInputIndexVertices as u64,
+            Some(&self.unit_vertices),
+            0,
+        );
+        command_encoder.set_vertex_bytes(
+            shaders::GPUISurfaceVertexInputIndex_GPUISurfaceVertexInputIndexViewportSize as u64,
+            mem::size_of::<shaders::vector_float2>() as u64,
+            [drawable_size.to_float2()].as_ptr() as *const c_void,
+        );
+
+        for surface in surfaces {
+            let origin = surface.bounds.origin() * scale_factor;
+            let source_size = vec2i(
+                surface.image_buffer.width() as i32,
+                surface.image_buffer.height() as i32,
+            );
+            let target_size = surface.bounds.size() * scale_factor;
+
+            assert_eq!(
+                surface.image_buffer.pixel_format_type(),
+                core_video::kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
+            );
+
+            let y_texture = self
+                .cv_texture_cache
+                .create_texture_from_image(
+                    surface.image_buffer.as_concrete_TypeRef(),
+                    ptr::null(),
+                    MTLPixelFormat::R8Unorm,
+                    surface.image_buffer.plane_width(0),
+                    surface.image_buffer.plane_height(0),
+                    0,
+                )
+                .unwrap();
+            let cb_cr_texture = self
+                .cv_texture_cache
+                .create_texture_from_image(
+                    surface.image_buffer.as_concrete_TypeRef(),
+                    ptr::null(),
+                    MTLPixelFormat::RG8Unorm,
+                    surface.image_buffer.plane_width(1),
+                    surface.image_buffer.plane_height(1),
+                    1,
+                )
+                .unwrap();
+
+            align_offset(offset);
+            let next_offset = *offset + mem::size_of::<shaders::GPUISurface>();
+            assert!(
+                next_offset <= INSTANCE_BUFFER_SIZE,
+                "instance buffer exhausted"
+            );
+
+            command_encoder.set_vertex_buffer(
+                shaders::GPUISurfaceVertexInputIndex_GPUISurfaceVertexInputIndexSurfaces as u64,
+                Some(&self.instances),
+                *offset as u64,
+            );
+            command_encoder.set_vertex_bytes(
+                shaders::GPUISurfaceVertexInputIndex_GPUISurfaceVertexInputIndexAtlasSize as u64,
+                mem::size_of::<shaders::vector_float2>() as u64,
+                [source_size.to_float2()].as_ptr() as *const c_void,
+            );
+            command_encoder.set_fragment_texture(
+                shaders::GPUISurfaceFragmentInputIndex_GPUISurfaceFragmentInputIndexYAtlas as u64,
+                Some(y_texture.as_texture_ref()),
+            );
+            command_encoder.set_fragment_texture(
+                shaders::GPUISurfaceFragmentInputIndex_GPUISurfaceFragmentInputIndexCbCrAtlas
+                    as u64,
+                Some(cb_cr_texture.as_texture_ref()),
+            );
+
+            unsafe {
+                let buffer_contents = (self.instances.contents() as *mut u8).add(*offset)
+                    as *mut shaders::GPUISurface;
+                std::ptr::write(
+                    buffer_contents,
+                    shaders::GPUISurface {
+                        origin: origin.to_float2(),
+                        target_size: target_size.to_float2(),
+                        source_size: source_size.to_float2(),
+                    },
+                );
+            }
+
+            command_encoder.draw_primitives(metal::MTLPrimitiveType::Triangle, 0, 6);
+            *offset = next_offset;
+        }
+    }
+
     fn render_path_sprites(
         &mut self,
         layer_id: usize,

crates/gpui/src/platform/mac/shaders/shaders.h 🔗

@@ -1,122 +1,125 @@
 #include <simd/simd.h>
 
-typedef struct
-{
-    vector_float2 viewport_size;
+typedef struct {
+  vector_float2 viewport_size;
 } GPUIUniforms;
 
-typedef enum
-{
-    GPUIQuadInputIndexVertices = 0,
-    GPUIQuadInputIndexQuads = 1,
-    GPUIQuadInputIndexUniforms = 2,
+typedef enum {
+  GPUIQuadInputIndexVertices = 0,
+  GPUIQuadInputIndexQuads = 1,
+  GPUIQuadInputIndexUniforms = 2,
 } GPUIQuadInputIndex;
 
-typedef struct
-{
-    vector_float2 origin;
-    vector_float2 size;
-    vector_uchar4 background_color;
-    float border_top;
-    float border_right;
-    float border_bottom;
-    float border_left;
-    vector_uchar4 border_color;
-    float corner_radius;
+typedef struct {
+  vector_float2 origin;
+  vector_float2 size;
+  vector_uchar4 background_color;
+  float border_top;
+  float border_right;
+  float border_bottom;
+  float border_left;
+  vector_uchar4 border_color;
+  float corner_radius;
 } GPUIQuad;
 
-typedef enum
-{
-    GPUIShadowInputIndexVertices = 0,
-    GPUIShadowInputIndexShadows = 1,
-    GPUIShadowInputIndexUniforms = 2,
+typedef enum {
+  GPUIShadowInputIndexVertices = 0,
+  GPUIShadowInputIndexShadows = 1,
+  GPUIShadowInputIndexUniforms = 2,
 } GPUIShadowInputIndex;
 
-typedef struct
-{
-    vector_float2 origin;
-    vector_float2 size;
-    float corner_radius;
-    float sigma;
-    vector_uchar4 color;
+typedef struct {
+  vector_float2 origin;
+  vector_float2 size;
+  float corner_radius;
+  float sigma;
+  vector_uchar4 color;
 } GPUIShadow;
 
-typedef enum
-{
-    GPUISpriteVertexInputIndexVertices = 0,
-    GPUISpriteVertexInputIndexSprites = 1,
-    GPUISpriteVertexInputIndexViewportSize = 2,
-    GPUISpriteVertexInputIndexAtlasSize = 3,
+typedef enum {
+  GPUISpriteVertexInputIndexVertices = 0,
+  GPUISpriteVertexInputIndexSprites = 1,
+  GPUISpriteVertexInputIndexViewportSize = 2,
+  GPUISpriteVertexInputIndexAtlasSize = 3,
 } GPUISpriteVertexInputIndex;
 
-typedef enum
-{
-    GPUISpriteFragmentInputIndexAtlas = 0,
+typedef enum {
+  GPUISpriteFragmentInputIndexAtlas = 0,
 } GPUISpriteFragmentInputIndex;
 
-typedef struct
-{
-    vector_float2 origin;
-    vector_float2 target_size;
-    vector_float2 source_size;
-    vector_float2 atlas_origin;
-    vector_uchar4 color;
-    uint8_t compute_winding;
+typedef struct {
+  vector_float2 origin;
+  vector_float2 target_size;
+  vector_float2 source_size;
+  vector_float2 atlas_origin;
+  vector_uchar4 color;
+  uint8_t compute_winding;
 } GPUISprite;
 
-typedef enum
-{
-    GPUIPathAtlasVertexInputIndexVertices = 0,
-    GPUIPathAtlasVertexInputIndexAtlasSize = 1,
+typedef enum {
+  GPUIPathAtlasVertexInputIndexVertices = 0,
+  GPUIPathAtlasVertexInputIndexAtlasSize = 1,
 } GPUIPathAtlasVertexInputIndex;
 
-typedef struct
-{
-    vector_float2 xy_position;
-    vector_float2 st_position;
-    vector_float2 clip_rect_origin;
-    vector_float2 clip_rect_size;
+typedef struct {
+  vector_float2 xy_position;
+  vector_float2 st_position;
+  vector_float2 clip_rect_origin;
+  vector_float2 clip_rect_size;
 } GPUIPathVertex;
 
-typedef enum
-{
-    GPUIImageVertexInputIndexVertices = 0,
-    GPUIImageVertexInputIndexImages = 1,
-    GPUIImageVertexInputIndexViewportSize = 2,
-    GPUIImageVertexInputIndexAtlasSize = 3,
+typedef enum {
+  GPUIImageVertexInputIndexVertices = 0,
+  GPUIImageVertexInputIndexImages = 1,
+  GPUIImageVertexInputIndexViewportSize = 2,
+  GPUIImageVertexInputIndexAtlasSize = 3,
 } GPUIImageVertexInputIndex;
 
-typedef enum
-{
-    GPUIImageFragmentInputIndexAtlas = 0,
+typedef enum {
+  GPUIImageFragmentInputIndexAtlas = 0,
 } GPUIImageFragmentInputIndex;
 
-typedef struct
-{
-    vector_float2 origin;
-    vector_float2 target_size;
-    vector_float2 source_size;
-    vector_float2 atlas_origin;
-    float border_top;
-    float border_right;
-    float border_bottom;
-    float border_left;
-    vector_uchar4 border_color;
-    float corner_radius;
+typedef struct {
+  vector_float2 origin;
+  vector_float2 target_size;
+  vector_float2 source_size;
+  vector_float2 atlas_origin;
+  float border_top;
+  float border_right;
+  float border_bottom;
+  float border_left;
+  vector_uchar4 border_color;
+  float corner_radius;
 } GPUIImage;
 
-typedef enum
-{
-    GPUIUnderlineInputIndexVertices = 0,
-    GPUIUnderlineInputIndexUnderlines = 1,
-    GPUIUnderlineInputIndexUniforms = 2,
+typedef enum {
+  GPUISurfaceVertexInputIndexVertices = 0,
+  GPUISurfaceVertexInputIndexSurfaces = 1,
+  GPUISurfaceVertexInputIndexViewportSize = 2,
+  GPUISurfaceVertexInputIndexAtlasSize = 3,
+} GPUISurfaceVertexInputIndex;
+
+typedef enum {
+  GPUISurfaceFragmentInputIndexYAtlas = 0,
+  GPUISurfaceFragmentInputIndexCbCrAtlas = 1,
+} GPUISurfaceFragmentInputIndex;
+
+typedef struct {
+  vector_float2 origin;
+  vector_float2 target_size;
+  vector_float2 source_size;
+} GPUISurface;
+
+typedef enum {
+  GPUIUnderlineInputIndexVertices = 0,
+  GPUIUnderlineInputIndexUnderlines = 1,
+  GPUIUnderlineInputIndexUniforms = 2,
 } GPUIUnderlineInputIndex;
 
-typedef struct
-{
-    vector_float2 origin;
-    vector_float2 size;
-    float thickness;
-    vector_uchar4 color;
-    uint8_t squiggly;
+typedef struct {
+  vector_float2 origin;
+  vector_float2 size;
+  float thickness;
+  vector_uchar4 color;
+  uint8_t squiggly;
 } GPUIUnderline;

crates/gpui/src/platform/mac/shaders/shaders.metal 🔗

@@ -263,6 +263,54 @@ fragment float4 image_fragment(
     return quad_sdf(input);
 }
 
+vertex QuadFragmentInput surface_vertex(
+    uint unit_vertex_id [[vertex_id]],
+    uint image_id [[instance_id]],
+    constant float2 *unit_vertices [[buffer(GPUISurfaceVertexInputIndexVertices)]],
+    constant GPUISurface *images [[buffer(GPUISurfaceVertexInputIndexSurfaces)]],
+    constant float2 *viewport_size [[buffer(GPUISurfaceVertexInputIndexViewportSize)]],
+    constant float2 *atlas_size [[buffer(GPUISurfaceVertexInputIndexAtlasSize)]]
+) {
+    float2 unit_vertex = unit_vertices[unit_vertex_id];
+    GPUISurface image = images[image_id];
+    float2 position = unit_vertex * image.target_size + image.origin;
+    float4 device_position = to_device_position(position, *viewport_size);
+    float2 atlas_position = (unit_vertex * image.source_size) / *atlas_size;
+
+    return QuadFragmentInput {
+        device_position,
+        atlas_position,
+        image.origin,
+        image.target_size,
+        float4(0.),
+        0.,
+        0.,
+        0.,
+        0.,
+        float4(0.),
+        0.,
+    };
+}
+
+fragment float4 surface_fragment(
+    QuadFragmentInput input [[stage_in]],
+    texture2d<float> y_atlas [[ texture(GPUISurfaceFragmentInputIndexYAtlas) ]],
+    texture2d<float> cb_cr_atlas [[ texture(GPUISurfaceFragmentInputIndexCbCrAtlas) ]]
+) {
+    constexpr sampler atlas_sampler(mag_filter::linear, min_filter::linear);
+    const float4x4 ycbcrToRGBTransform = float4x4(
+        float4(+1.0000f, +1.0000f, +1.0000f, +0.0000f),
+        float4(+0.0000f, -0.3441f, +1.7720f, +0.0000f),
+        float4(+1.4020f, -0.7141f, +0.0000f, +0.0000f),
+        float4(-0.7010f, +0.5291f, -0.8860f, +1.0000f)
+    );
+    float4 ycbcr = float4(y_atlas.sample(atlas_sampler, input.atlas_position).r,
+                          cb_cr_atlas.sample(atlas_sampler, input.atlas_position).rg, 1.0);
+
+    input.background_color = ycbcrToRGBTransform * ycbcr;
+    return quad_sdf(input);
+}
+
 struct PathAtlasVertexOutput {
     float4 position [[position]];
     float2 st_position;

crates/gpui/src/platform/mac/status_item.rs 🔗

@@ -0,0 +1,367 @@
+use crate::{
+    geometry::{
+        rect::RectF,
+        vector::{vec2f, Vector2F},
+    },
+    platform::{
+        self,
+        mac::{platform::NSViewLayerContentsRedrawDuringViewResize, renderer::Renderer},
+    },
+    Event, FontSystem, Scene,
+};
+use cocoa::{
+    appkit::{NSScreen, NSSquareStatusItemLength, NSStatusBar, NSStatusItem, NSView, NSWindow},
+    base::{id, nil, YES},
+    foundation::{NSPoint, NSRect, NSSize},
+};
+use ctor::ctor;
+use foreign_types::ForeignTypeRef;
+use objc::{
+    class,
+    declare::ClassDecl,
+    msg_send,
+    rc::StrongPtr,
+    runtime::{Class, Object, Protocol, Sel},
+    sel, sel_impl,
+};
+use std::{
+    cell::RefCell,
+    ffi::c_void,
+    ptr,
+    rc::{Rc, Weak},
+    sync::Arc,
+};
+
+static mut VIEW_CLASS: *const Class = ptr::null();
+const STATE_IVAR: &str = "state";
+
+#[ctor]
+unsafe fn build_classes() {
+    VIEW_CLASS = {
+        let mut decl = ClassDecl::new("GPUIStatusItemView", class!(NSView)).unwrap();
+        decl.add_ivar::<*mut c_void>(STATE_IVAR);
+
+        decl.add_method(sel!(dealloc), dealloc_view as extern "C" fn(&Object, Sel));
+
+        decl.add_method(
+            sel!(mouseDown:),
+            handle_view_event as extern "C" fn(&Object, Sel, id),
+        );
+        decl.add_method(
+            sel!(mouseUp:),
+            handle_view_event as extern "C" fn(&Object, Sel, id),
+        );
+        decl.add_method(
+            sel!(rightMouseDown:),
+            handle_view_event as extern "C" fn(&Object, Sel, id),
+        );
+        decl.add_method(
+            sel!(rightMouseUp:),
+            handle_view_event as extern "C" fn(&Object, Sel, id),
+        );
+        decl.add_method(
+            sel!(otherMouseDown:),
+            handle_view_event as extern "C" fn(&Object, Sel, id),
+        );
+        decl.add_method(
+            sel!(otherMouseUp:),
+            handle_view_event as extern "C" fn(&Object, Sel, id),
+        );
+        decl.add_method(
+            sel!(mouseMoved:),
+            handle_view_event as extern "C" fn(&Object, Sel, id),
+        );
+        decl.add_method(
+            sel!(mouseDragged:),
+            handle_view_event as extern "C" fn(&Object, Sel, id),
+        );
+        decl.add_method(
+            sel!(scrollWheel:),
+            handle_view_event as extern "C" fn(&Object, Sel, id),
+        );
+        decl.add_method(
+            sel!(flagsChanged:),
+            handle_view_event as extern "C" fn(&Object, Sel, id),
+        );
+        decl.add_method(
+            sel!(makeBackingLayer),
+            make_backing_layer as extern "C" fn(&Object, Sel) -> id,
+        );
+        decl.add_method(
+            sel!(viewDidChangeEffectiveAppearance),
+            view_did_change_effective_appearance as extern "C" fn(&Object, Sel),
+        );
+
+        decl.add_protocol(Protocol::get("CALayerDelegate").unwrap());
+        decl.add_method(
+            sel!(displayLayer:),
+            display_layer as extern "C" fn(&Object, Sel, id),
+        );
+
+        decl.register()
+    };
+}
+
+pub struct StatusItem(Rc<RefCell<StatusItemState>>);
+
+struct StatusItemState {
+    native_item: StrongPtr,
+    native_view: StrongPtr,
+    renderer: Renderer,
+    scene: Option<Scene>,
+    event_callback: Option<Box<dyn FnMut(Event) -> bool>>,
+    appearance_changed_callback: Option<Box<dyn FnMut()>>,
+}
+
+impl StatusItem {
+    pub fn add(fonts: Arc<dyn FontSystem>) -> Self {
+        unsafe {
+            let renderer = Renderer::new(false, fonts);
+            let status_bar = NSStatusBar::systemStatusBar(nil);
+            let native_item =
+                StrongPtr::retain(status_bar.statusItemWithLength_(NSSquareStatusItemLength));
+
+            let button = native_item.button();
+            let _: () = msg_send![button, setHidden: YES];
+
+            let native_view = msg_send![VIEW_CLASS, alloc];
+            let state = Rc::new(RefCell::new(StatusItemState {
+                native_item,
+                native_view: StrongPtr::new(native_view),
+                renderer,
+                scene: None,
+                event_callback: None,
+                appearance_changed_callback: None,
+            }));
+
+            let parent_view = button.superview().superview();
+            NSView::initWithFrame_(
+                native_view,
+                NSRect::new(NSPoint::new(0., 0.), NSView::frame(parent_view).size),
+            );
+            (*native_view).set_ivar(
+                STATE_IVAR,
+                Weak::into_raw(Rc::downgrade(&state)) as *const c_void,
+            );
+            native_view.setWantsBestResolutionOpenGLSurface_(YES);
+            native_view.setWantsLayer(YES);
+            let _: () = msg_send![
+                native_view,
+                setLayerContentsRedrawPolicy: NSViewLayerContentsRedrawDuringViewResize
+            ];
+
+            parent_view.addSubview_(native_view);
+
+            {
+                let state = state.borrow();
+                let layer = state.renderer.layer();
+                let scale_factor = state.scale_factor();
+                let size = state.content_size() * scale_factor;
+                layer.set_contents_scale(scale_factor.into());
+                layer.set_drawable_size(metal::CGSize::new(size.x().into(), size.y().into()));
+            }
+
+            Self(state)
+        }
+    }
+}
+
+impl platform::Window for StatusItem {
+    fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
+        self
+    }
+
+    fn on_event(&mut self, callback: Box<dyn FnMut(crate::Event) -> bool>) {
+        self.0.borrow_mut().event_callback = Some(callback);
+    }
+
+    fn on_appearance_changed(&mut self, callback: Box<dyn FnMut()>) {
+        self.0.borrow_mut().appearance_changed_callback = Some(callback);
+    }
+
+    fn on_active_status_change(&mut self, _: Box<dyn FnMut(bool)>) {}
+
+    fn on_resize(&mut self, _: Box<dyn FnMut()>) {}
+
+    fn on_fullscreen(&mut self, _: Box<dyn FnMut(bool)>) {}
+
+    fn on_should_close(&mut self, _: Box<dyn FnMut() -> bool>) {}
+
+    fn on_close(&mut self, _: Box<dyn FnOnce()>) {}
+
+    fn set_input_handler(&mut self, _: Box<dyn crate::InputHandler>) {}
+
+    fn prompt(
+        &self,
+        _: crate::PromptLevel,
+        _: &str,
+        _: &[&str],
+    ) -> postage::oneshot::Receiver<usize> {
+        unimplemented!()
+    }
+
+    fn activate(&self) {
+        unimplemented!()
+    }
+
+    fn set_title(&mut self, _: &str) {
+        unimplemented!()
+    }
+
+    fn set_edited(&mut self, _: bool) {
+        unimplemented!()
+    }
+
+    fn show_character_palette(&self) {
+        unimplemented!()
+    }
+
+    fn minimize(&self) {
+        unimplemented!()
+    }
+
+    fn zoom(&self) {
+        unimplemented!()
+    }
+
+    fn toggle_full_screen(&self) {
+        unimplemented!()
+    }
+
+    fn bounds(&self) -> RectF {
+        self.0.borrow().bounds()
+    }
+
+    fn content_size(&self) -> Vector2F {
+        self.0.borrow().content_size()
+    }
+
+    fn scale_factor(&self) -> f32 {
+        self.0.borrow().scale_factor()
+    }
+
+    fn titlebar_height(&self) -> f32 {
+        0.
+    }
+
+    fn present_scene(&mut self, scene: Scene) {
+        self.0.borrow_mut().scene = Some(scene);
+        unsafe {
+            let _: () = msg_send![*self.0.borrow().native_view, setNeedsDisplay: YES];
+        }
+    }
+
+    fn appearance(&self) -> crate::Appearance {
+        unsafe {
+            let appearance: id =
+                msg_send![self.0.borrow().native_item.button(), effectiveAppearance];
+            crate::Appearance::from_native(appearance)
+        }
+    }
+}
+
+impl StatusItemState {
+    fn bounds(&self) -> RectF {
+        unsafe {
+            let window: id = msg_send![self.native_item.button(), window];
+            let screen_frame = window.screen().visibleFrame();
+            let window_frame = NSWindow::frame(window);
+            let origin = vec2f(
+                window_frame.origin.x as f32,
+                (window_frame.origin.y - screen_frame.size.height - window_frame.size.height)
+                    as f32,
+            );
+            let size = vec2f(
+                window_frame.size.width as f32,
+                window_frame.size.height as f32,
+            );
+            RectF::new(origin, size)
+        }
+    }
+
+    fn content_size(&self) -> Vector2F {
+        unsafe {
+            let NSSize { width, height, .. } =
+                NSView::frame(self.native_item.button().superview().superview()).size;
+            vec2f(width as f32, height as f32)
+        }
+    }
+
+    fn scale_factor(&self) -> f32 {
+        unsafe {
+            let window: id = msg_send![self.native_item.button(), window];
+            NSScreen::backingScaleFactor(window.screen()) as f32
+        }
+    }
+}
+
+extern "C" fn dealloc_view(this: &Object, _: Sel) {
+    unsafe {
+        drop_state(this);
+
+        let _: () = msg_send![super(this, class!(NSView)), dealloc];
+    }
+}
+
+extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
+    unsafe {
+        if let Some(state) = get_state(this).upgrade() {
+            let mut state_borrow = state.as_ref().borrow_mut();
+            if let Some(event) =
+                Event::from_native(native_event, Some(state_borrow.content_size().y()))
+            {
+                if let Some(mut callback) = state_borrow.event_callback.take() {
+                    drop(state_borrow);
+                    callback(event);
+                    state.borrow_mut().event_callback = Some(callback);
+                }
+            }
+        }
+    }
+}
+
+extern "C" fn make_backing_layer(this: &Object, _: Sel) -> id {
+    if let Some(state) = unsafe { get_state(this).upgrade() } {
+        let state = state.borrow();
+        state.renderer.layer().as_ptr() as id
+    } else {
+        nil
+    }
+}
+
+extern "C" fn display_layer(this: &Object, _: Sel, _: id) {
+    unsafe {
+        if let Some(state) = get_state(this).upgrade() {
+            let mut state = state.borrow_mut();
+            if let Some(scene) = state.scene.take() {
+                state.renderer.render(&scene);
+            }
+        }
+    }
+}
+
+extern "C" fn view_did_change_effective_appearance(this: &Object, _: Sel) {
+    unsafe {
+        if let Some(state) = get_state(this).upgrade() {
+            let mut state_borrow = state.as_ref().borrow_mut();
+            if let Some(mut callback) = state_borrow.appearance_changed_callback.take() {
+                drop(state_borrow);
+                callback();
+                state.borrow_mut().appearance_changed_callback = Some(callback);
+            }
+        }
+    }
+}
+
+unsafe fn get_state(object: &Object) -> Weak<RefCell<StatusItemState>> {
+    let raw: *mut c_void = *object.get_ivar(STATE_IVAR);
+    let weak1 = Weak::from_raw(raw as *mut RefCell<StatusItemState>);
+    let weak2 = weak1.clone();
+    let _ = Weak::into_raw(weak1);
+    weak2
+}
+
+unsafe fn drop_state(object: &Object) {
+    let raw: *const c_void = *object.get_ivar(STATE_IVAR);
+    Weak::from_raw(raw as *const RefCell<StatusItemState>);
+}

crates/gpui/src/platform/mac/window.rs 🔗

@@ -1,4 +1,3 @@
-use super::{geometry::RectFExt, renderer::Renderer};
 use crate::{
     executor,
     geometry::{
@@ -6,25 +5,30 @@ use crate::{
         vector::{vec2f, Vector2F},
     },
     keymap::Keystroke,
-    platform::{self, Event, WindowBounds, WindowContext},
+    mac::platform::NSViewLayerContentsRedrawDuringViewResize,
+    platform::{
+        self,
+        mac::{geometry::RectFExt, renderer::Renderer},
+        Event, WindowBounds,
+    },
     InputHandler, KeyDownEvent, ModifiersChangedEvent, MouseButton, MouseButtonEvent,
-    MouseMovedEvent, Scene,
+    MouseMovedEvent, Scene, WindowKind,
 };
 use block::ConcreteBlock;
 use cocoa::{
     appkit::{
         CGPoint, NSApplication, NSBackingStoreBuffered, NSScreen, NSView, NSViewHeightSizable,
-        NSViewWidthSizable, NSWindow, NSWindowButton, NSWindowStyleMask,
+        NSViewWidthSizable, NSWindow, NSWindowButton, NSWindowCollectionBehavior,
+        NSWindowStyleMask,
     },
     base::{id, nil},
     foundation::{
         NSAutoreleasePool, NSInteger, NSNotFound, NSPoint, NSRect, NSSize, NSString, NSUInteger,
     },
-    quartzcore::AutoresizingMask,
 };
 use core_graphics::display::CGRect;
 use ctor::ctor;
-use foreign_types::ForeignType as _;
+use foreign_types::ForeignTypeRef;
 use objc::{
     class,
     declare::ClassDecl,
@@ -51,8 +55,25 @@ use std::{
 const WINDOW_STATE_IVAR: &str = "windowState";
 
 static mut WINDOW_CLASS: *const Class = ptr::null();
+static mut PANEL_CLASS: *const Class = ptr::null();
 static mut VIEW_CLASS: *const Class = ptr::null();
 
+#[allow(non_upper_case_globals)]
+const NSWindowStyleMaskNonactivatingPanel: NSWindowStyleMask =
+    unsafe { NSWindowStyleMask::from_bits_unchecked(1 << 7) };
+#[allow(non_upper_case_globals)]
+const NSNormalWindowLevel: NSInteger = 0;
+#[allow(non_upper_case_globals)]
+const NSPopUpWindowLevel: NSInteger = 101;
+#[allow(non_upper_case_globals)]
+const NSTrackingMouseMoved: NSUInteger = 0x02;
+#[allow(non_upper_case_globals)]
+const NSTrackingActiveAlways: NSUInteger = 0x80;
+#[allow(non_upper_case_globals)]
+const NSTrackingInVisibleRect: NSUInteger = 0x200;
+#[allow(non_upper_case_globals)]
+const NSWindowAnimationBehaviorUtilityWindow: NSInteger = 4;
+
 #[repr(C)]
 #[derive(Copy, Clone, Debug)]
 struct NSRange {
@@ -103,55 +124,10 @@ unsafe impl objc::Encode for NSRange {
     }
 }
 
-#[allow(non_upper_case_globals)]
-const NSViewLayerContentsRedrawDuringViewResize: NSInteger = 2;
-
 #[ctor]
 unsafe fn build_classes() {
-    WINDOW_CLASS = {
-        let mut decl = ClassDecl::new("GPUIWindow", class!(NSWindow)).unwrap();
-        decl.add_ivar::<*mut c_void>(WINDOW_STATE_IVAR);
-        decl.add_method(sel!(dealloc), dealloc_window as extern "C" fn(&Object, Sel));
-        decl.add_method(
-            sel!(canBecomeMainWindow),
-            yes as extern "C" fn(&Object, Sel) -> BOOL,
-        );
-        decl.add_method(
-            sel!(canBecomeKeyWindow),
-            yes as extern "C" fn(&Object, Sel) -> BOOL,
-        );
-        decl.add_method(
-            sel!(sendEvent:),
-            send_event as extern "C" fn(&Object, Sel, id),
-        );
-        decl.add_method(
-            sel!(windowDidResize:),
-            window_did_resize as extern "C" fn(&Object, Sel, id),
-        );
-        decl.add_method(
-            sel!(windowWillEnterFullScreen:),
-            window_will_enter_fullscreen as extern "C" fn(&Object, Sel, id),
-        );
-        decl.add_method(
-            sel!(windowWillExitFullScreen:),
-            window_will_exit_fullscreen as extern "C" fn(&Object, Sel, id),
-        );
-        decl.add_method(
-            sel!(windowDidBecomeKey:),
-            window_did_change_key_status as extern "C" fn(&Object, Sel, id),
-        );
-        decl.add_method(
-            sel!(windowDidResignKey:),
-            window_did_change_key_status as extern "C" fn(&Object, Sel, id),
-        );
-        decl.add_method(
-            sel!(windowShouldClose:),
-            window_should_close as extern "C" fn(&Object, Sel, id) -> BOOL,
-        );
-        decl.add_method(sel!(close), close_window as extern "C" fn(&Object, Sel));
-        decl.register()
-    };
-
+    WINDOW_CLASS = build_window_class("GPUIWindow", class!(NSWindow));
+    PANEL_CLASS = build_window_class("GPUIPanel", class!(NSPanel));
     VIEW_CLASS = {
         let mut decl = ClassDecl::new("GPUIView", class!(NSView)).unwrap();
         decl.add_ivar::<*mut c_void>(WINDOW_STATE_IVAR);
@@ -265,6 +241,10 @@ unsafe fn build_classes() {
             attributed_substring_for_proposed_range
                 as extern "C" fn(&Object, Sel, NSRange, *mut c_void) -> id,
         );
+        decl.add_method(
+            sel!(viewDidChangeEffectiveAppearance),
+            view_did_change_effective_appearance as extern "C" fn(&Object, Sel),
+        );
 
         // Suppress beep on keystrokes with modifier keys.
         decl.add_method(
@@ -276,6 +256,50 @@ unsafe fn build_classes() {
     };
 }
 
+unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const Class {
+    let mut decl = ClassDecl::new(name, superclass).unwrap();
+    decl.add_ivar::<*mut c_void>(WINDOW_STATE_IVAR);
+    decl.add_method(sel!(dealloc), dealloc_window as extern "C" fn(&Object, Sel));
+    decl.add_method(
+        sel!(canBecomeMainWindow),
+        yes as extern "C" fn(&Object, Sel) -> BOOL,
+    );
+    decl.add_method(
+        sel!(canBecomeKeyWindow),
+        yes as extern "C" fn(&Object, Sel) -> BOOL,
+    );
+    decl.add_method(
+        sel!(sendEvent:),
+        send_event as extern "C" fn(&Object, Sel, id),
+    );
+    decl.add_method(
+        sel!(windowDidResize:),
+        window_did_resize as extern "C" fn(&Object, Sel, id),
+    );
+    decl.add_method(
+        sel!(windowWillEnterFullScreen:),
+        window_will_enter_fullscreen as extern "C" fn(&Object, Sel, id),
+    );
+    decl.add_method(
+        sel!(windowWillExitFullScreen:),
+        window_will_exit_fullscreen as extern "C" fn(&Object, Sel, id),
+    );
+    decl.add_method(
+        sel!(windowDidBecomeKey:),
+        window_did_change_key_status as extern "C" fn(&Object, Sel, id),
+    );
+    decl.add_method(
+        sel!(windowDidResignKey:),
+        window_did_change_key_status as extern "C" fn(&Object, Sel, id),
+    );
+    decl.add_method(
+        sel!(windowShouldClose:),
+        window_should_close as extern "C" fn(&Object, Sel, id) -> BOOL,
+    );
+    decl.add_method(sel!(close), close_window as extern "C" fn(&Object, Sel));
+    decl.register()
+}
+
 pub struct Window(Rc<RefCell<WindowState>>);
 
 ///Used to track what the IME does when we send it a keystroke.
@@ -299,6 +323,7 @@ struct WindowState {
     fullscreen_callback: Option<Box<dyn FnMut(bool)>>,
     should_close_callback: Option<Box<dyn FnMut() -> bool>>,
     close_callback: Option<Box<dyn FnOnce()>>,
+    appearance_changed_callback: Option<Box<dyn FnMut()>>,
     input_handler: Option<Box<dyn InputHandler>>,
     pending_key_down: Option<(KeyDownEvent, Option<InsertText>)>,
     performed_key_equivalent: bool,
@@ -306,9 +331,7 @@ struct WindowState {
     executor: Rc<executor::Foreground>,
     scene_to_render: Option<Scene>,
     renderer: Renderer,
-    command_queue: metal::CommandQueue,
     last_fresh_keydown: Option<Keystroke>,
-    layer: id,
     traffic_light_position: Option<Vector2F>,
     previous_modifiers_changed_event: Option<Event>,
     //State tracking what the IME did after the last request
@@ -329,58 +352,59 @@ impl Window {
         executor: Rc<executor::Foreground>,
         fonts: Arc<dyn platform::FontSystem>,
     ) -> Self {
-        const PIXEL_FORMAT: metal::MTLPixelFormat = metal::MTLPixelFormat::BGRA8Unorm;
-
         unsafe {
             let pool = NSAutoreleasePool::new(nil);
 
-            let frame = match options.bounds {
-                WindowBounds::Maximized => RectF::new(Default::default(), vec2f(1024., 768.)),
-                WindowBounds::Fixed(rect) => rect,
-            }
-            .to_ns_rect();
-            let mut style_mask = NSWindowStyleMask::NSClosableWindowMask
-                | NSWindowStyleMask::NSMiniaturizableWindowMask
-                | NSWindowStyleMask::NSResizableWindowMask
-                | NSWindowStyleMask::NSTitledWindowMask;
-
-            if options.titlebar_appears_transparent {
-                style_mask |= NSWindowStyleMask::NSFullSizeContentViewWindowMask;
+            let mut style_mask;
+            if let Some(titlebar) = options.titlebar.as_ref() {
+                style_mask = NSWindowStyleMask::NSClosableWindowMask
+                    | NSWindowStyleMask::NSMiniaturizableWindowMask
+                    | NSWindowStyleMask::NSResizableWindowMask
+                    | NSWindowStyleMask::NSTitledWindowMask;
+
+                if titlebar.appears_transparent {
+                    style_mask |= NSWindowStyleMask::NSFullSizeContentViewWindowMask;
+                }
+            } else {
+                style_mask = NSWindowStyleMask::NSTitledWindowMask
+                    | NSWindowStyleMask::NSFullSizeContentViewWindowMask;
             }
 
-            let native_window: id = msg_send![WINDOW_CLASS, alloc];
+            let native_window: id = match options.kind {
+                WindowKind::Normal => msg_send![WINDOW_CLASS, alloc],
+                WindowKind::PopUp => {
+                    style_mask |= NSWindowStyleMaskNonactivatingPanel;
+                    msg_send![PANEL_CLASS, alloc]
+                }
+            };
             let native_window = native_window.initWithContentRect_styleMask_backing_defer_(
-                frame,
+                RectF::new(Default::default(), vec2f(1024., 768.)).to_ns_rect(),
                 style_mask,
                 NSBackingStoreBuffered,
                 NO,
             );
             assert!(!native_window.is_null());
 
-            if matches!(options.bounds, WindowBounds::Maximized) {
-                let screen = native_window.screen();
-                native_window.setFrame_display_(screen.visibleFrame(), YES);
+            let screen = native_window.screen();
+            match options.bounds {
+                WindowBounds::Maximized => {
+                    native_window.setFrame_display_(screen.visibleFrame(), YES);
+                }
+                WindowBounds::Fixed(top_left_bounds) => {
+                    let frame = screen.visibleFrame();
+                    let bottom_left_bounds = RectF::new(
+                        vec2f(
+                            top_left_bounds.origin_x(),
+                            frame.size.height as f32
+                                - top_left_bounds.origin_y()
+                                - top_left_bounds.height(),
+                        ),
+                        top_left_bounds.size(),
+                    );
+                    native_window.setFrame_display_(bottom_left_bounds.to_ns_rect(), YES);
+                }
             }
 
-            let device: metal::Device = if let Some(device) = metal::Device::system_default() {
-                device
-            } else {
-                log::error!("unable to access a compatible graphics device");
-                std::process::exit(1);
-            };
-
-            let layer: id = msg_send![class!(CAMetalLayer), layer];
-            let _: () = msg_send![layer, setDevice: device.as_ptr()];
-            let _: () = msg_send![layer, setPixelFormat: PIXEL_FORMAT];
-            let _: () = msg_send![layer, setAllowsNextDrawableTimeout: NO];
-            let _: () = msg_send![layer, setNeedsDisplayOnBoundsChange: YES];
-            let _: () = msg_send![layer, setPresentsWithTransaction: YES];
-            let _: () = msg_send![
-                layer,
-                setAutoresizingMask: AutoresizingMask::WIDTH_SIZABLE
-                    | AutoresizingMask::HEIGHT_SIZABLE
-            ];
-
             let native_view: id = msg_send![VIEW_CLASS, alloc];
             let native_view = NSView::init(native_view);
             assert!(!native_view.is_null());
@@ -394,22 +418,19 @@ impl Window {
                 close_callback: None,
                 activate_callback: None,
                 fullscreen_callback: None,
+                appearance_changed_callback: None,
                 input_handler: None,
                 pending_key_down: None,
                 performed_key_equivalent: false,
                 synthetic_drag_counter: 0,
                 executor,
                 scene_to_render: Default::default(),
-                renderer: Renderer::new(
-                    device.clone(),
-                    PIXEL_FORMAT,
-                    get_scale_factor(native_window),
-                    fonts,
-                ),
-                command_queue: device.new_command_queue(),
+                renderer: Renderer::new(true, fonts),
                 last_fresh_keydown: None,
-                layer,
-                traffic_light_position: options.traffic_light_position,
+                traffic_light_position: options
+                    .titlebar
+                    .as_ref()
+                    .and_then(|titlebar| titlebar.traffic_light_position),
                 previous_modifiers_changed_event: None,
                 ime_state: ImeState::None,
                 ime_text: None,
@@ -425,13 +446,28 @@ impl Window {
                 Rc::into_raw(window.0.clone()) as *const c_void,
             );
 
-            if let Some(title) = options.title.as_ref() {
+            if let Some(title) = options.titlebar.as_ref().and_then(|t| t.title) {
                 native_window.setTitle_(NSString::alloc(nil).init_str(title));
             }
-            if options.titlebar_appears_transparent {
+
+            native_window.setMovable_(options.is_movable as BOOL);
+
+            if options
+                .titlebar
+                .map_or(true, |titlebar| titlebar.appears_transparent)
+            {
                 native_window.setTitlebarAppearsTransparent_(YES);
             }
-            native_window.setAcceptsMouseMovedEvents_(YES);
+
+            let tracking_area: id = msg_send![class!(NSTrackingArea), alloc];
+            let _: () = msg_send![
+                tracking_area,
+                initWithRect: NSRect::new(NSPoint::new(0., 0.), NSSize::new(0., 0.))
+                options: NSTrackingMouseMoved | NSTrackingActiveAlways | NSTrackingInVisibleRect
+                owner: native_view
+                userInfo: nil
+            ];
+            let _: () = msg_send![native_view, addTrackingArea: tracking_area.autorelease()];
 
             native_view.setAutoresizingMask_(NSViewWidthSizable | NSViewHeightSizable);
             native_view.setWantsBestResolutionOpenGLSurface_(YES);
@@ -450,7 +486,24 @@ impl Window {
             native_window.setContentView_(native_view.autorelease());
             native_window.makeFirstResponder_(native_view);
 
-            native_window.center();
+            if options.center {
+                native_window.center();
+            }
+
+            match options.kind {
+                WindowKind::Normal => native_window.setLevel_(NSNormalWindowLevel),
+                WindowKind::PopUp => {
+                    native_window.setLevel_(NSPopUpWindowLevel);
+                    let _: () = msg_send![
+                        native_window,
+                        setAnimationBehavior: NSWindowAnimationBehaviorUtilityWindow
+                    ];
+                    native_window.setCollectionBehavior_(
+                        NSWindowCollectionBehavior::NSWindowCollectionBehaviorCanJoinAllSpaces |
+                        NSWindowCollectionBehavior::NSWindowCollectionBehaviorFullScreenAuxiliary
+                    );
+                }
+            }
             native_window.makeKeyAndOrderFront_(nil);
 
             window.0.borrow().move_traffic_light();
@@ -637,11 +690,13 @@ impl platform::Window for Window {
             })
             .detach();
     }
-}
 
-impl platform::WindowContext for Window {
-    fn size(&self) -> Vector2F {
-        self.0.as_ref().borrow().size()
+    fn bounds(&self) -> RectF {
+        self.0.as_ref().borrow().bounds()
+    }
+
+    fn content_size(&self) -> Vector2F {
+        self.0.as_ref().borrow().content_size()
     }
 
     fn scale_factor(&self) -> f32 {
@@ -655,6 +710,17 @@ impl platform::WindowContext for Window {
     fn titlebar_height(&self) -> f32 {
         self.0.as_ref().borrow().titlebar_height()
     }
+
+    fn appearance(&self) -> crate::Appearance {
+        unsafe {
+            let appearance: id = msg_send![self.0.borrow().native_window, effectiveAppearance];
+            crate::Appearance::from_native(appearance)
+        }
+    }
+
+    fn on_appearance_changed(&mut self, callback: Box<dyn FnMut()>) {
+        self.0.borrow_mut().appearance_changed_callback = Some(callback);
+    }
 }
 
 impl WindowState {
@@ -701,10 +767,25 @@ impl WindowState {
             }
         }
     }
-}
 
-impl platform::WindowContext for WindowState {
-    fn size(&self) -> Vector2F {
+    fn bounds(&self) -> RectF {
+        unsafe {
+            let screen_frame = self.native_window.screen().visibleFrame();
+            let window_frame = NSWindow::frame(self.native_window);
+            let origin = vec2f(
+                window_frame.origin.x as f32,
+                (window_frame.origin.y - screen_frame.size.height - window_frame.size.height)
+                    as f32,
+            );
+            let size = vec2f(
+                window_frame.size.width as f32,
+                window_frame.size.height as f32,
+            );
+            RectF::new(origin, size)
+        }
+    }
+
+    fn content_size(&self) -> Vector2F {
         let NSSize { width, height, .. } =
             unsafe { NSView::frame(self.native_window.contentView()) }.size;
         vec2f(width as f32, height as f32)
@@ -781,7 +862,8 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
 
     let mut window_state_borrow = window_state.as_ref().borrow_mut();
 
-    let event = unsafe { Event::from_native(native_event, Some(window_state_borrow.size().y())) };
+    let event =
+        unsafe { Event::from_native(native_event, Some(window_state_borrow.content_size().y())) };
 
     if let Some(event) = event {
         if key_equivalent {
@@ -873,7 +955,8 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
     let weak_window_state = Rc::downgrade(&window_state);
     let mut window_state_borrow = window_state.as_ref().borrow_mut();
 
-    let event = unsafe { Event::from_native(native_event, Some(window_state_borrow.size().y())) };
+    let event =
+        unsafe { Event::from_native(native_event, Some(window_state_borrow.content_size().y())) };
     if let Some(event) = event {
         match &event {
             Event::MouseMoved(
@@ -992,16 +1075,30 @@ fn window_fullscreen_changed(this: &Object, is_fullscreen: bool) {
 }
 
 extern "C" fn window_did_change_key_status(this: &Object, selector: Sel, _: id) {
-    let is_active = if selector == sel!(windowDidBecomeKey:) {
-        true
-    } else if selector == sel!(windowDidResignKey:) {
-        false
-    } else {
-        unreachable!();
-    };
-
     let window_state = unsafe { get_window_state(this) };
-    let executor = window_state.as_ref().borrow().executor.clone();
+    let window_state_borrow = window_state.borrow();
+    let is_active = unsafe { window_state_borrow.native_window.isKeyWindow() == YES };
+
+    // When opening a pop-up while the application isn't active, Cocoa sends a spurious
+    // `windowDidBecomeKey` message to the previous key window even though that window
+    // isn't actually key. This causes a bug if the application is later activated while
+    // the pop-up is still open, making it impossible to activate the previous key window
+    // even if the pop-up gets closed. The only way to activate it again is to de-activate
+    // the app and re-activate it, which is a pretty bad UX.
+    // The following code detects the spurious event and invokes `resignKeyWindow`:
+    // in theory, we're not supposed to invoke this method manually but it balances out
+    // the spurious `becomeKeyWindow` event and helps us work around that bug.
+    if selector == sel!(windowDidBecomeKey:) {
+        if !is_active {
+            unsafe {
+                let _: () = msg_send![window_state_borrow.native_window, resignKeyWindow];
+                return;
+            }
+        }
+    }
+
+    let executor = window_state_borrow.executor.clone();
+    drop(window_state_borrow);
     executor
         .spawn(async move {
             let mut window_state_borrow = window_state.as_ref().borrow_mut();
@@ -1049,7 +1146,7 @@ extern "C" fn close_window(this: &Object, _: Sel) {
 extern "C" fn make_backing_layer(this: &Object, _: Sel) -> id {
     let window_state = unsafe { get_window_state(this) };
     let window_state = window_state.as_ref().borrow();
-    window_state.layer
+    window_state.renderer.layer().as_ptr() as id
 }
 
 extern "C" fn view_did_change_backing_properties(this: &Object, _: Sel) {
@@ -1058,14 +1155,20 @@ extern "C" fn view_did_change_backing_properties(this: &Object, _: Sel) {
 
     unsafe {
         let scale_factor = window_state_borrow.scale_factor() as f64;
-        let size = window_state_borrow.size();
+        let size = window_state_borrow.content_size();
         let drawable_size: NSSize = NSSize {
             width: size.x() as f64 * scale_factor,
             height: size.y() as f64 * scale_factor,
         };
 
-        let _: () = msg_send![window_state_borrow.layer, setContentsScale: scale_factor];
-        let _: () = msg_send![window_state_borrow.layer, setDrawableSize: drawable_size];
+        let _: () = msg_send![
+            window_state_borrow.renderer.layer(),
+            setContentsScale: scale_factor
+        ];
+        let _: () = msg_send![
+            window_state_borrow.renderer.layer(),
+            setDrawableSize: drawable_size
+        ];
     }
 
     if let Some(mut callback) = window_state_borrow.resize_callback.take() {
@@ -1079,7 +1182,7 @@ extern "C" fn set_frame_size(this: &Object, _: Sel, size: NSSize) {
     let window_state = unsafe { get_window_state(this) };
     let window_state_borrow = window_state.as_ref().borrow();
 
-    if window_state_borrow.size() == vec2f(size.width as f32, size.height as f32) {
+    if window_state_borrow.content_size() == vec2f(size.width as f32, size.height as f32) {
         return;
     }
 
@@ -1094,7 +1197,10 @@ extern "C" fn set_frame_size(this: &Object, _: Sel, size: NSSize) {
     };
 
     unsafe {
-        let _: () = msg_send![window_state_borrow.layer, setDrawableSize: drawable_size];
+        let _: () = msg_send![
+            window_state_borrow.renderer.layer(),
+            setDrawableSize: drawable_size
+        ];
     }
 
     drop(window_state_borrow);
@@ -1110,25 +1216,8 @@ extern "C" fn display_layer(this: &Object, _: Sel, _: id) {
     unsafe {
         let window_state = get_window_state(this);
         let mut window_state = window_state.as_ref().borrow_mut();
-
         if let Some(scene) = window_state.scene_to_render.take() {
-            let drawable: &metal::MetalDrawableRef = msg_send![window_state.layer, nextDrawable];
-            let command_queue = window_state.command_queue.clone();
-            let command_buffer = command_queue.new_command_buffer();
-
-            let size = window_state.size();
-            let scale_factor = window_state.scale_factor();
-
-            window_state.renderer.render(
-                &scene,
-                size * scale_factor,
-                command_buffer,
-                drawable.texture(),
-            );
-
-            command_buffer.commit();
-            command_buffer.wait_until_completed();
-            drawable.present();
+            window_state.renderer.render(&scene);
         };
     }
 }
@@ -1301,6 +1390,18 @@ extern "C" fn do_command_by_selector(this: &Object, _: Sel, _: Sel) {
     }
 }
 
+extern "C" fn view_did_change_effective_appearance(this: &Object, _: Sel) {
+    unsafe {
+        let state = get_window_state(this);
+        let mut state_borrow = state.as_ref().borrow_mut();
+        if let Some(mut callback) = state_borrow.appearance_changed_callback.take() {
+            drop(state_borrow);
+            callback();
+            state.borrow_mut().appearance_changed_callback = Some(callback);
+        }
+    }
+}
+
 async fn synthetic_drag(
     window_state: Weak<RefCell<WindowState>>,
     drag_id: usize,

crates/gpui/src/platform/test.rs 🔗

@@ -1,6 +1,9 @@
 use super::{AppVersion, CursorStyle, WindowBounds};
 use crate::{
-    geometry::vector::{vec2f, Vector2F},
+    geometry::{
+        rect::RectF,
+        vector::{vec2f, Vector2F},
+    },
     keymap, Action, ClipboardItem,
 };
 use anyhow::{anyhow, Result};
@@ -120,6 +123,14 @@ impl super::Platform for Platform {
 
     fn activate(&self, _ignoring_other_apps: bool) {}
 
+    fn hide(&self) {}
+
+    fn hide_other_apps(&self) {}
+
+    fn unhide_other_apps(&self) {}
+
+    fn quit(&self) {}
+
     fn open_window(
         &self,
         _: usize,
@@ -136,13 +147,9 @@ impl super::Platform for Platform {
         None
     }
 
-    fn hide(&self) {}
-
-    fn hide_other_apps(&self) {}
-
-    fn unhide_other_apps(&self) {}
-
-    fn quit(&self) {}
+    fn add_status_item(&self) -> Box<dyn crate::Window> {
+        Box::new(Window::new(vec2f(24., 24.)))
+    }
 
     fn write_to_clipboard(&self, item: ClipboardItem) {
         *self.current_clipboard_item.lock() = Some(item);
@@ -224,24 +231,6 @@ impl super::Dispatcher for Dispatcher {
     }
 }
 
-impl super::WindowContext for Window {
-    fn size(&self) -> Vector2F {
-        self.size
-    }
-
-    fn scale_factor(&self) -> f32 {
-        self.scale_factor
-    }
-
-    fn titlebar_height(&self) -> f32 {
-        24.
-    }
-
-    fn present_scene(&mut self, scene: crate::Scene) {
-        self.current_scene = Some(scene);
-    }
-}
-
 impl super::Window for Window {
     fn as_any_mut(&mut self) -> &mut dyn Any {
         self
@@ -296,6 +285,32 @@ impl super::Window for Window {
     fn zoom(&self) {}
 
     fn toggle_full_screen(&self) {}
+
+    fn bounds(&self) -> RectF {
+        RectF::new(Default::default(), self.size)
+    }
+
+    fn content_size(&self) -> Vector2F {
+        self.size
+    }
+
+    fn scale_factor(&self) -> f32 {
+        self.scale_factor
+    }
+
+    fn titlebar_height(&self) -> f32 {
+        24.
+    }
+
+    fn present_scene(&mut self, scene: crate::Scene) {
+        self.current_scene = Some(scene);
+    }
+
+    fn appearance(&self) -> crate::Appearance {
+        crate::Appearance::Light
+    }
+
+    fn on_appearance_changed(&mut self, _: Box<dyn FnMut()>) {}
 }
 
 pub fn platform() -> Platform {

crates/gpui/src/presenter.rs 🔗

@@ -8,13 +8,14 @@ use crate::{
     platform::{CursorStyle, Event},
     scene::{
         ClickRegionEvent, CursorRegion, DownOutRegionEvent, DownRegionEvent, DragRegionEvent,
-        HoverRegionEvent, MouseRegionEvent, MoveRegionEvent, UpOutRegionEvent, UpRegionEvent,
+        HoverRegionEvent, MouseRegionEvent, MoveRegionEvent, ScrollWheelRegionEvent,
+        UpOutRegionEvent, UpRegionEvent,
     },
     text_layout::TextLayoutCache,
-    Action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AssetCache, ElementBox, Entity,
-    FontSystem, ModelHandle, MouseButton, MouseMovedEvent, MouseRegion, MouseRegionId, ParentId,
-    ReadModel, ReadView, RenderContext, RenderParams, Scene, UpgradeModelHandle, UpgradeViewHandle,
-    View, ViewHandle, WeakModelHandle, WeakViewHandle,
+    Action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, Appearance, AssetCache, ElementBox,
+    Entity, FontSystem, ModelHandle, MouseButton, MouseMovedEvent, MouseRegion, MouseRegionId,
+    ParentId, ReadModel, ReadView, RenderContext, RenderParams, Scene, UpgradeModelHandle,
+    UpgradeViewHandle, View, ViewHandle, WeakModelHandle, WeakViewHandle,
 };
 use collections::{HashMap, HashSet};
 use pathfinder_geometry::vector::{vec2f, Vector2F};
@@ -36,16 +37,18 @@ pub struct Presenter {
     asset_cache: Arc<AssetCache>,
     last_mouse_moved_event: Option<Event>,
     hovered_region_ids: HashSet<MouseRegionId>,
-    clicked_regions: Vec<MouseRegion>,
+    clicked_region_ids: HashSet<MouseRegionId>,
     clicked_button: Option<MouseButton>,
     mouse_position: Vector2F,
     titlebar_height: f32,
+    appearance: Appearance,
 }
 
 impl Presenter {
     pub fn new(
         window_id: usize,
         titlebar_height: f32,
+        appearance: Appearance,
         font_cache: Arc<FontCache>,
         text_layout_cache: TextLayoutCache,
         asset_cache: Arc<AssetCache>,
@@ -53,7 +56,7 @@ impl Presenter {
     ) -> Self {
         Self {
             window_id,
-            rendered_views: cx.render_views(window_id, titlebar_height),
+            rendered_views: cx.render_views(window_id, titlebar_height, appearance),
             cursor_regions: Default::default(),
             mouse_regions: Default::default(),
             font_cache,
@@ -61,19 +64,22 @@ impl Presenter {
             asset_cache,
             last_mouse_moved_event: None,
             hovered_region_ids: Default::default(),
-            clicked_regions: Vec::new(),
+            clicked_region_ids: Default::default(),
             clicked_button: None,
             mouse_position: vec2f(0., 0.),
             titlebar_height,
+            appearance,
         }
     }
 
     pub fn invalidate(
         &mut self,
         invalidation: &mut WindowInvalidation,
+        appearance: Appearance,
         cx: &mut MutableAppContext,
     ) {
         cx.start_frame();
+        self.appearance = appearance;
         for view_id in &invalidation.removed {
             invalidation.updated.remove(view_id);
             self.rendered_views.remove(view_id);
@@ -86,24 +92,24 @@ impl Presenter {
                     view_id: *view_id,
                     titlebar_height: self.titlebar_height,
                     hovered_region_ids: self.hovered_region_ids.clone(),
-                    clicked_region_ids: self.clicked_button.map(|button| {
-                        (
-                            self.clicked_regions
-                                .iter()
-                                .filter_map(MouseRegion::id)
-                                .collect(),
-                            button,
-                        )
-                    }),
+                    clicked_region_ids: self
+                        .clicked_button
+                        .map(|button| (self.clicked_region_ids.clone(), button)),
                     refreshing: false,
+                    appearance,
                 })
                 .unwrap(),
             );
         }
     }
 
-    pub fn refresh(&mut self, invalidation: &mut WindowInvalidation, cx: &mut MutableAppContext) {
-        self.invalidate(invalidation, cx);
+    pub fn refresh(
+        &mut self,
+        invalidation: &mut WindowInvalidation,
+        appearance: Appearance,
+        cx: &mut MutableAppContext,
+    ) {
+        self.invalidate(invalidation, appearance, cx);
         for (view_id, view) in &mut self.rendered_views {
             if !invalidation.updated.contains(view_id) {
                 *view = cx
@@ -112,16 +118,11 @@ impl Presenter {
                         view_id: *view_id,
                         titlebar_height: self.titlebar_height,
                         hovered_region_ids: self.hovered_region_ids.clone(),
-                        clicked_region_ids: self.clicked_button.map(|button| {
-                            (
-                                self.clicked_regions
-                                    .iter()
-                                    .filter_map(MouseRegion::id)
-                                    .collect(),
-                                button,
-                            )
-                        }),
+                        clicked_region_ids: self
+                            .clicked_button
+                            .map(|button| (self.clicked_region_ids.clone(), button)),
                         refreshing: true,
+                        appearance,
                     })
                     .unwrap();
             }
@@ -184,16 +185,11 @@ impl Presenter {
             view_stack: Vec::new(),
             refreshing,
             hovered_region_ids: self.hovered_region_ids.clone(),
-            clicked_region_ids: self.clicked_button.map(|button| {
-                (
-                    self.clicked_regions
-                        .iter()
-                        .filter_map(MouseRegion::id)
-                        .collect(),
-                    button,
-                )
-            }),
+            clicked_region_ids: self
+                .clicked_button
+                .map(|button| (self.clicked_region_ids.clone(), button)),
             titlebar_height: self.titlebar_height,
+            appearance: self.appearance,
             window_size,
             app: cx,
         }
@@ -235,6 +231,7 @@ impl Presenter {
     ) -> bool {
         if let Some(root_view_id) = cx.root_view_id(self.window_id) {
             let mut events_to_send = Vec::new();
+            let mut invalidated_views: HashSet<usize> = Default::default();
 
             // 1. Allocate the correct set of GPUI events generated from the platform events
             //  -> These are usually small: [Mouse Down] or [Mouse up, Click] or [Mouse Moved, Mouse Dragged?]
@@ -248,16 +245,23 @@ impl Presenter {
 
                     // If there is already clicked_button stored, don't replace it.
                     if self.clicked_button.is_none() {
-                        self.clicked_regions = self
+                        self.clicked_region_ids = self
                             .mouse_regions
                             .iter()
                             .filter_map(|(region, _)| {
-                                region
-                                    .bounds
-                                    .contains_point(e.position)
-                                    .then(|| region.clone())
+                                if region.bounds.contains_point(e.position) {
+                                    Some(region.id())
+                                } else {
+                                    None
+                                }
                             })
                             .collect();
+
+                        // Clicked status is used when rendering views via the RenderContext.
+                        // So when it changes, these views need to be rerendered
+                        for clicked_region_id in self.clicked_region_ids.iter() {
+                            invalidated_views.insert(clicked_region_id.view_id());
+                        }
                         self.clicked_button = Some(e.button);
                     }
 
@@ -341,6 +345,12 @@ impl Presenter {
 
                     self.last_mouse_moved_event = Some(event.clone());
                 }
+                Event::ScrollWheel(e) => {
+                    events_to_send.push(MouseRegionEvent::ScrollWheel(ScrollWheelRegionEvent {
+                        region: Default::default(),
+                        platform_event: e.clone(),
+                    }))
+                }
 
                 _ => {}
             }
@@ -349,7 +359,6 @@ impl Presenter {
                 self.mouse_position = position;
             }
 
-            let mut invalidated_views: HashSet<usize> = Default::default();
             let mut any_event_handled = false;
             // 2. Process the raw mouse events into region events
             for mut region_event in events_to_send {
@@ -375,23 +384,21 @@ impl Presenter {
                                 top_most_depth = Some(depth);
                             }
 
-                            if let Some(region_id) = region.id() {
-                                // This unwrap relies on short circuiting boolean expressions
-                                // The right side of the && is only executed when contains_mouse
-                                // is true, and we know above that when contains_mouse is true
-                                // top_most_depth is set
-                                if contains_mouse && depth == top_most_depth.unwrap() {
-                                    //Ensure that hover entrance events aren't sent twice
-                                    if self.hovered_region_ids.insert(region_id) {
-                                        valid_regions.push(region.clone());
-                                        invalidated_views.insert(region.view_id);
-                                    }
-                                } else {
-                                    // Ensure that hover exit events aren't sent twice
-                                    if self.hovered_region_ids.remove(&region_id) {
-                                        valid_regions.push(region.clone());
-                                        invalidated_views.insert(region.view_id);
-                                    }
+                            // This unwrap relies on short circuiting boolean expressions
+                            // The right side of the && is only executed when contains_mouse
+                            // is true, and we know above that when contains_mouse is true
+                            // top_most_depth is set
+                            if contains_mouse && depth == top_most_depth.unwrap() {
+                                //Ensure that hover entrance events aren't sent twice
+                                if self.hovered_region_ids.insert(region.id()) {
+                                    valid_regions.push(region.clone());
+                                    invalidated_views.insert(region.id().view_id());
+                                }
+                            } else {
+                                // Ensure that hover exit events aren't sent twice
+                                if self.hovered_region_ids.remove(&region.id()) {
+                                    valid_regions.push(region.clone());
+                                    invalidated_views.insert(region.id().view_id());
                                 }
                             }
                         }
@@ -404,21 +411,30 @@ impl Presenter {
                             .unwrap_or(false)
                         {
                             // Clear clicked regions and clicked button
-                            let clicked_regions =
-                                std::mem::replace(&mut self.clicked_regions, Vec::new());
+                            let clicked_region_ids =
+                                std::mem::replace(&mut self.clicked_region_ids, Default::default());
+                            // Clicked status is used when rendering views via the RenderContext.
+                            // So when it changes, these views need to be rerendered
+                            for clicked_region_id in clicked_region_ids.iter() {
+                                invalidated_views.insert(clicked_region_id.view_id());
+                            }
                             self.clicked_button = None;
 
                             // Find regions which still overlap with the mouse since the last MouseDown happened
-                            for clicked_region in clicked_regions.into_iter().rev() {
-                                if clicked_region.bounds.contains_point(e.position) {
-                                    valid_regions.push(clicked_region);
+                            for (mouse_region, _) in self.mouse_regions.iter().rev() {
+                                if clicked_region_ids.contains(&mouse_region.id()) {
+                                    if mouse_region.bounds.contains_point(self.mouse_position) {
+                                        valid_regions.push(mouse_region.clone());
+                                    }
                                 }
                             }
                         }
                     }
                     MouseRegionEvent::Drag(_) => {
-                        for clicked_region in self.clicked_regions.iter().rev() {
-                            valid_regions.push(clicked_region.clone());
+                        for (mouse_region, _) in self.mouse_regions.iter().rev() {
+                            if self.clicked_region_ids.contains(&mouse_region.id()) {
+                                valid_regions.push(mouse_region.clone());
+                            }
                         }
                     }
 
@@ -447,18 +463,18 @@ impl Presenter {
 
                     region_event.set_region(valid_region.bounds);
                     if let MouseRegionEvent::Hover(e) = &mut region_event {
-                        e.started = valid_region
-                            .id()
-                            .map(|region_id| hovered_region_ids.contains(&region_id))
-                            .unwrap_or(false)
+                        e.started = hovered_region_ids.contains(&valid_region.id())
                     }
-                    // Handle Down events if the MouseRegion has a Click handler. This makes the api more intuitive as you would
+                    // Handle Down events if the MouseRegion has a Click or Drag handler. This makes the api more intuitive as you would
                     // not expect a MouseRegion to be transparent to Down events if it also has a Click handler.
                     // This behavior can be overridden by adding a Down handler that calls cx.propogate_event
                     if let MouseRegionEvent::Down(e) = &region_event {
                         if valid_region
                             .handlers
                             .contains_handler(MouseRegionEvent::click_disc(), Some(e.button))
+                            || valid_region
+                                .handlers
+                                .contains_handler(MouseRegionEvent::drag_disc(), Some(e.button))
                         {
                             event_cx.handled = true;
                         }
@@ -466,8 +482,10 @@ impl Presenter {
 
                     if let Some(callback) = valid_region.handlers.get(&region_event.handler_key()) {
                         event_cx.handled = true;
-                        event_cx.invalidated_views.insert(valid_region.view_id);
-                        event_cx.with_current_view(valid_region.view_id, {
+                        event_cx
+                            .invalidated_views
+                            .insert(valid_region.id().view_id());
+                        event_cx.with_current_view(valid_region.id().view_id(), {
                             let region_event = region_event.clone();
                             |cx| {
                                 callback(region_event, cx);
@@ -545,8 +563,9 @@ pub struct LayoutContext<'a> {
     pub refreshing: bool,
     pub window_size: Vector2F,
     titlebar_height: f32,
+    appearance: Appearance,
     hovered_region_ids: HashSet<MouseRegionId>,
-    clicked_region_ids: Option<(Vec<MouseRegionId>, MouseButton)>,
+    clicked_region_ids: Option<(HashSet<MouseRegionId>, MouseButton)>,
 }
 
 impl<'a> LayoutContext<'a> {
@@ -619,6 +638,7 @@ impl<'a> LayoutContext<'a> {
                 hovered_region_ids: self.hovered_region_ids.clone(),
                 clicked_region_ids: self.clicked_region_ids.clone(),
                 refreshing: self.refreshing,
+                appearance: self.appearance,
             };
             f(view, &mut render_cx)
         })

crates/gpui/src/scene.rs 🔗

@@ -1,6 +1,8 @@
 mod mouse_region;
 mod mouse_region_event;
 
+#[cfg(debug_assertions)]
+use collections::HashSet;
 use serde::Deserialize;
 use serde_json::json;
 use std::{borrow::Cow, sync::Arc};
@@ -10,7 +12,7 @@ use crate::{
     fonts::{FontId, GlyphId},
     geometry::{rect::RectF, vector::Vector2F},
     json::ToJson,
-    platform::CursorStyle,
+    platform::{current::Surface, CursorStyle},
     ImageData,
 };
 pub use mouse_region::*;
@@ -20,6 +22,8 @@ pub struct Scene {
     scale_factor: f32,
     stacking_contexts: Vec<StackingContext>,
     active_stacking_context_stack: Vec<usize>,
+    #[cfg(debug_assertions)]
+    mouse_region_ids: HashSet<MouseRegionId>,
 }
 
 struct StackingContext {
@@ -34,6 +38,7 @@ pub struct Layer {
     quads: Vec<Quad>,
     underlines: Vec<Underline>,
     images: Vec<Image>,
+    surfaces: Vec<Surface>,
     shadows: Vec<Shadow>,
     glyphs: Vec<Glyph>,
     image_glyphs: Vec<ImageGlyph>,
@@ -177,6 +182,8 @@ impl Scene {
             scale_factor,
             stacking_contexts: vec![stacking_context],
             active_stacking_context_stack: vec![0],
+            #[cfg(debug_assertions)]
+            mouse_region_ids: Default::default(),
         }
     }
 
@@ -241,7 +248,24 @@ impl Scene {
 
     pub fn push_mouse_region(&mut self, region: MouseRegion) {
         if can_draw(region.bounds) {
-            self.active_layer().push_mouse_region(region);
+            // Ensure that Regions cannot be added to a scene with the same region id.
+            #[cfg(debug_assertions)]
+            let region_id;
+            #[cfg(debug_assertions)]
+            {
+                region_id = region.id();
+            }
+
+            if self.active_layer().push_mouse_region(region) {
+                #[cfg(debug_assertions)]
+                {
+                    if !self.mouse_region_ids.insert(region_id) {
+                        let tag_name = region_id.tag_type_name();
+                        panic!("Same MouseRegionId: {region_id:?} inserted multiple times to the same scene. \
+                            Will cause problems! Look for MouseRegion that uses Tag: {tag_name}");
+                    }
+                }
+            }
         }
     }
 
@@ -249,6 +273,10 @@ impl Scene {
         self.active_layer().push_image(image)
     }
 
+    pub fn push_surface(&mut self, surface: Surface) {
+        self.active_layer().push_surface(surface)
+    }
+
     pub fn push_underline(&mut self, underline: Underline) {
         self.active_layer().push_underline(underline)
     }
@@ -329,6 +357,7 @@ impl Layer {
             quads: Default::default(),
             underlines: Default::default(),
             images: Default::default(),
+            surfaces: Default::default(),
             shadows: Default::default(),
             image_glyphs: Default::default(),
             glyphs: Default::default(),
@@ -364,15 +393,17 @@ impl Layer {
         }
     }
 
-    fn push_mouse_region(&mut self, region: MouseRegion) {
+    fn push_mouse_region(&mut self, region: MouseRegion) -> bool {
         if let Some(bounds) = region
             .bounds
             .intersection(self.clip_bounds.unwrap_or(region.bounds))
         {
             if can_draw(bounds) {
                 self.mouse_regions.push(region);
+                return true;
             }
         }
+        false
     }
 
     fn push_underline(&mut self, underline: Underline) {
@@ -395,6 +426,16 @@ impl Layer {
         self.images.as_slice()
     }
 
+    fn push_surface(&mut self, surface: Surface) {
+        if can_draw(surface.bounds) {
+            self.surfaces.push(surface);
+        }
+    }
+
+    pub fn surfaces(&self) -> &[Surface] {
+        self.surfaces.as_slice()
+    }
+
     fn push_shadow(&mut self, shadow: Shadow) {
         if can_draw(shadow.bounds) {
             self.shadows.push(shadow);
@@ -536,11 +577,8 @@ impl ToJson for Border {
 }
 
 impl MouseRegion {
-    pub fn id(&self) -> Option<MouseRegionId> {
-        self.discriminant.map(|discriminant| MouseRegionId {
-            view_id: self.view_id,
-            discriminant,
-        })
+    pub fn id(&self) -> MouseRegionId {
+        self.id
     }
 }
 

crates/gpui/src/scene/mouse_region.rs 🔗

@@ -1,4 +1,4 @@
-use std::{any::TypeId, mem::Discriminant, rc::Rc};
+use std::{any::TypeId, fmt::Debug, mem::Discriminant, rc::Rc};
 
 use collections::HashMap;
 
@@ -6,50 +6,51 @@ use pathfinder_geometry::rect::RectF;
 
 use crate::{EventContext, MouseButton};
 
-use super::mouse_region_event::{
-    ClickRegionEvent, DownOutRegionEvent, DownRegionEvent, DragRegionEvent, HoverRegionEvent,
-    MouseRegionEvent, MoveRegionEvent, UpOutRegionEvent, UpRegionEvent,
+use super::{
+    mouse_region_event::{
+        ClickRegionEvent, DownOutRegionEvent, DownRegionEvent, DragRegionEvent, HoverRegionEvent,
+        MouseRegionEvent, MoveRegionEvent, UpOutRegionEvent, UpRegionEvent,
+    },
+    ScrollWheelRegionEvent,
 };
 
-#[derive(Clone, Default)]
+#[derive(Clone)]
 pub struct MouseRegion {
-    pub view_id: usize,
-    pub discriminant: Option<(TypeId, usize)>,
+    pub id: MouseRegionId,
     pub bounds: RectF,
     pub handlers: HandlerSet,
     pub hoverable: bool,
 }
 
 impl MouseRegion {
-    pub fn new(view_id: usize, discriminant: Option<(TypeId, usize)>, bounds: RectF) -> Self {
-        Self::from_handlers(view_id, discriminant, bounds, Default::default())
+    /// Region ID is used to track semantically equivalent mouse regions across render passes.
+    /// e.g. if you have mouse handlers attached to a list item type, then each item of the list
+    /// should pass a different (consistent) region_id. If you have one big region that covers your
+    /// whole component, just pass the view_id again.
+    pub fn new<Tag: 'static>(view_id: usize, region_id: usize, bounds: RectF) -> Self {
+        Self::from_handlers::<Tag>(view_id, region_id, bounds, Default::default())
     }
 
-    pub fn from_handlers(
-        view_id: usize,
-        discriminant: Option<(TypeId, usize)>,
-        bounds: RectF,
-        handlers: HandlerSet,
-    ) -> Self {
-        Self {
-            view_id,
-            discriminant,
-            bounds,
-            handlers,
-            hoverable: true,
-        }
+    pub fn handle_all<Tag: 'static>(view_id: usize, region_id: usize, bounds: RectF) -> Self {
+        Self::from_handlers::<Tag>(view_id, region_id, bounds, HandlerSet::capture_all())
     }
 
-    pub fn handle_all(
+    pub fn from_handlers<Tag: 'static>(
         view_id: usize,
-        discriminant: Option<(TypeId, usize)>,
+        region_id: usize,
         bounds: RectF,
+        handlers: HandlerSet,
     ) -> Self {
         Self {
-            view_id,
-            discriminant,
+            id: MouseRegionId {
+                view_id,
+                tag: TypeId::of::<Tag>(),
+                region_id,
+                #[cfg(debug_assertions)]
+                tag_type_name: std::any::type_name::<Tag>(),
+            },
             bounds,
-            handlers: HandlerSet::capture_all(),
+            handlers,
             hoverable: true,
         }
     }
@@ -124,6 +125,14 @@ impl MouseRegion {
         self
     }
 
+    pub fn on_scroll(
+        mut self,
+        handler: impl Fn(ScrollWheelRegionEvent, &mut EventContext) + 'static,
+    ) -> Self {
+        self.handlers = self.handlers.on_scroll(handler);
+        self
+    }
+
     pub fn with_hoverable(mut self, is_hoverable: bool) -> Self {
         self.hoverable = is_hoverable;
         self
@@ -132,8 +141,32 @@ impl MouseRegion {
 
 #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
 pub struct MouseRegionId {
-    pub view_id: usize,
-    pub discriminant: (TypeId, usize),
+    view_id: usize,
+    tag: TypeId,
+    region_id: usize,
+    #[cfg(debug_assertions)]
+    tag_type_name: &'static str,
+}
+
+impl MouseRegionId {
+    pub(crate) fn new<Tag: 'static>(view_id: usize, region_id: usize) -> Self {
+        MouseRegionId {
+            view_id,
+            region_id,
+            tag: TypeId::of::<Tag>(),
+            #[cfg(debug_assertions)]
+            tag_type_name: std::any::type_name::<Tag>(),
+        }
+    }
+
+    pub fn view_id(&self) -> usize {
+        self.view_id
+    }
+
+    #[cfg(debug_assertions)]
+    pub fn tag_type_name(&self) -> &'static str {
+        self.tag_type_name
+    }
 }
 
 #[derive(Clone, Default)]
@@ -345,4 +378,22 @@ impl HandlerSet {
             }));
         self
     }
+
+    pub fn on_scroll(
+        mut self,
+        handler: impl Fn(ScrollWheelRegionEvent, &mut EventContext) + 'static,
+    ) -> Self {
+        self.set.insert((MouseRegionEvent::scroll_wheel_disc(), None),
+            Rc::new(move |region_event, cx| {
+                if let MouseRegionEvent::ScrollWheel(e) = region_event {
+                    handler(e, cx);
+                } else {
+                    panic!(
+                        "Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::ScrollWheel, found {:?}",
+                        region_event
+                    );
+                }
+            }));
+        self
+    }
 }

crates/gpui/src/scene/mouse_region_event.rs 🔗

@@ -168,7 +168,7 @@ impl MouseRegionEvent {
     pub fn is_capturable(&self) -> bool {
         match self {
             MouseRegionEvent::Move(_) => true,
-            MouseRegionEvent::Drag(_) => false,
+            MouseRegionEvent::Drag(_) => true,
             MouseRegionEvent::Hover(_) => false,
             MouseRegionEvent::Down(_) => true,
             MouseRegionEvent::Up(_) => true,

crates/gpui/src/views/select.rs 🔗

@@ -109,7 +109,7 @@ impl View for Select {
             Default::default()
         };
         let mut result = Flex::column().with_child(
-            MouseEventHandler::new::<Header, _, _>(self.handle.id(), cx, |mouse_state, cx| {
+            MouseEventHandler::<Header>::new(self.handle.id(), cx, |mouse_state, cx| {
                 Container::new((self.render_item)(
                     self.selected_item_ix,
                     ItemType::Header,
@@ -137,22 +137,18 @@ impl View for Select {
                                     let selected_item_ix = this.selected_item_ix;
                                     range.end = range.end.min(this.item_count);
                                     items.extend(range.map(|ix| {
-                                        MouseEventHandler::new::<Item, _, _>(
-                                            ix,
-                                            cx,
-                                            |mouse_state, cx| {
-                                                (this.render_item)(
-                                                    ix,
-                                                    if ix == selected_item_ix {
-                                                        ItemType::Selected
-                                                    } else {
-                                                        ItemType::Unselected
-                                                    },
-                                                    mouse_state.hovered,
-                                                    cx,
-                                                )
-                                            },
-                                        )
+                                        MouseEventHandler::<Item>::new(ix, cx, |mouse_state, cx| {
+                                            (this.render_item)(
+                                                ix,
+                                                if ix == selected_item_ix {
+                                                    ItemType::Selected
+                                                } else {
+                                                    ItemType::Unselected
+                                                },
+                                                mouse_state.hovered,
+                                                cx,
+                                            )
+                                        })
                                         .on_click(MouseButton::Left, move |_, cx| {
                                             cx.dispatch_action(SelectItem(ix))
                                         })

crates/live_kit/Cargo.toml 🔗

@@ -0,0 +1,22 @@
+[package]
+name = "live_kit"
+version = "0.1.0"
+edition = "2021"
+description = "Bindings to LiveKit Swift client SDK"
+
+[lib]
+path = "src/live_kit.rs"
+doctest = false
+
+[dependencies]
+media = { path = "../media" }
+
+anyhow = "1.0.38"
+core-foundation = "0.9.3"
+core-graphics = "0.22.3"
+futures = "0.3"
+parking_lot = "0.11.1"
+
+[build-dependencies]
+serde = { version = "1.0", features = ["derive", "rc"] }
+serde_json = { version = "1.0", features = ["preserve_order"] }

crates/live_kit/LiveKitBridge/.gitignore 🔗

@@ -0,0 +1,9 @@
+.DS_Store
+/.build
+/Packages
+/*.xcodeproj
+xcuserdata/
+DerivedData/
+.swiftpm/config/registries.json
+.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
+.netrc

crates/live_kit/LiveKitBridge/Package.resolved 🔗

@@ -0,0 +1,52 @@
+{
+  "object": {
+    "pins": [
+      {
+        "package": "LiveKit",
+        "repositoryURL": "https://github.com/livekit/client-sdk-swift.git",
+        "state": {
+          "branch": null,
+          "revision": "5cc3c001779ab147199ce3ea0dce465b846368b4",
+          "version": null
+        }
+      },
+      {
+        "package": "Promises",
+        "repositoryURL": "https://github.com/google/promises.git",
+        "state": {
+          "branch": null,
+          "revision": "3e4e743631e86c8c70dbc6efdc7beaa6e90fd3bb",
+          "version": "2.1.1"
+        }
+      },
+      {
+        "package": "WebRTC",
+        "repositoryURL": "https://github.com/webrtc-sdk/Specs.git",
+        "state": {
+          "branch": null,
+          "revision": "5225f2de4b6d0098803b3a0e55b255a41f293dad",
+          "version": "104.5112.2"
+        }
+      },
+      {
+        "package": "swift-log",
+        "repositoryURL": "https://github.com/apple/swift-log.git",
+        "state": {
+          "branch": null,
+          "revision": "6fe203dc33195667ce1759bf0182975e4653ba1c",
+          "version": "1.4.4"
+        }
+      },
+      {
+        "package": "SwiftProtobuf",
+        "repositoryURL": "https://github.com/apple/swift-protobuf.git",
+        "state": {
+          "branch": null,
+          "revision": "b8230909dedc640294d7324d37f4c91ad3dcf177",
+          "version": "1.20.1"
+        }
+      }
+    ]
+  },
+  "version": 1
+}

crates/live_kit/LiveKitBridge/Package.swift 🔗

@@ -0,0 +1,27 @@
+// swift-tools-version: 5.5
+
+import PackageDescription
+
+let package = Package(
+    name: "LiveKitBridge",
+    platforms: [
+        .macOS(.v10_15)
+    ],
+    products: [
+        // Products define the executables and libraries a package produces, and make them visible to other packages.
+        .library(
+            name: "LiveKitBridge",
+            type: .static,
+            targets: ["LiveKitBridge"]),
+    ],
+    dependencies: [
+        .package(url: "https://github.com/livekit/client-sdk-swift.git", revision: "5cc3c001779ab147199ce3ea0dce465b846368b4"),
+    ],
+    targets: [
+        // Targets are the basic building blocks of a package. A target can define a module or a test suite.
+        // Targets can depend on other targets in this package, and on products in packages this package depends on.
+        .target(
+            name: "LiveKitBridge",
+            dependencies: [.product(name: "LiveKit", package: "client-sdk-swift")]),
+    ]
+)

crates/live_kit/LiveKitBridge/Sources/LiveKitBridge/LiveKitBridge.swift 🔗

@@ -0,0 +1,105 @@
+import Foundation
+import LiveKit
+import WebRTC
+
+class LKRoomDelegate: RoomDelegate {
+    var data: UnsafeRawPointer
+    var onDidSubscribeToRemoteVideoTrack: @convention(c) (UnsafeRawPointer, UnsafeRawPointer) -> Void
+    
+    init(data: UnsafeRawPointer, onDidSubscribeToRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, UnsafeRawPointer) -> Void) {
+        self.data = data
+        self.onDidSubscribeToRemoteVideoTrack = onDidSubscribeToRemoteVideoTrack
+    }
+
+    func room(_ room: Room, participant: RemoteParticipant, didSubscribe publication: RemoteTrackPublication, track: Track) {
+        if track.kind == .video {
+            self.onDidSubscribeToRemoteVideoTrack(self.data, Unmanaged.passRetained(track).toOpaque())
+        }
+    }
+}
+
+class LKVideoRenderer: NSObject, VideoRenderer {
+    var data: UnsafeRawPointer
+    var onFrame: @convention(c) (UnsafeRawPointer, CVPixelBuffer) -> Void
+    var onDrop: @convention(c) (UnsafeRawPointer) -> Void
+    var adaptiveStreamIsEnabled: Bool = false
+    var adaptiveStreamSize: CGSize = .zero
+
+    init(data: UnsafeRawPointer, onFrame: @escaping @convention(c) (UnsafeRawPointer, CVPixelBuffer) -> Void, onDrop: @escaping @convention(c) (UnsafeRawPointer) -> Void) {
+        self.data = data
+        self.onFrame = onFrame
+        self.onDrop = onDrop
+    }
+
+    deinit {
+        self.onDrop(self.data)
+    }
+
+    func setSize(_ size: CGSize) {
+        print("Called setSize", size);
+    }
+
+    func renderFrame(_ frame: RTCVideoFrame?) {
+        let buffer = frame?.buffer as? RTCCVPixelBuffer
+        if let pixelBuffer = buffer?.pixelBuffer {
+            self.onFrame(self.data, pixelBuffer)
+        }
+    }
+}
+
+@_cdecl("LKRelease")
+public func LKRelease(ptr: UnsafeRawPointer)  {
+    let _ = Unmanaged<AnyObject>.fromOpaque(ptr).takeRetainedValue()
+}
+
+@_cdecl("LKRoomDelegateCreate")
+public func LKRoomDelegateCreate(data: UnsafeRawPointer, onDidSubscribeToRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, UnsafeRawPointer) -> Void) -> UnsafeMutableRawPointer {
+    let delegate = LKRoomDelegate(data: data, onDidSubscribeToRemoteVideoTrack: onDidSubscribeToRemoteVideoTrack)
+    return Unmanaged.passRetained(delegate).toOpaque()
+}
+
+@_cdecl("LKRoomCreate")
+public func LKRoomCreate(delegate: UnsafeRawPointer) -> UnsafeMutableRawPointer  {
+    let delegate = Unmanaged<LKRoomDelegate>.fromOpaque(delegate).takeUnretainedValue()
+    return Unmanaged.passRetained(Room(delegate: delegate)).toOpaque()
+}
+
+@_cdecl("LKRoomConnect")
+public func LKRoomConnect(room: UnsafeRawPointer, url: CFString, token: CFString, callback: @escaping @convention(c) (UnsafeRawPointer, CFString?) -> Void, callback_data: UnsafeRawPointer) {
+    let room = Unmanaged<Room>.fromOpaque(room).takeUnretainedValue()
+
+    room.connect(url as String, token as String).then { _ in
+        callback(callback_data, UnsafeRawPointer(nil) as! CFString?)
+    }.catch { error in
+        callback(callback_data, error.localizedDescription as CFString)
+    }
+}
+
+@_cdecl("LKRoomPublishVideoTrack")
+public func LKRoomPublishVideoTrack(room: UnsafeRawPointer, track: UnsafeRawPointer, callback: @escaping @convention(c) (UnsafeRawPointer, CFString?) -> Void, callback_data: UnsafeRawPointer) {
+    let room = Unmanaged<Room>.fromOpaque(room).takeUnretainedValue()
+    let track = Unmanaged<LocalVideoTrack>.fromOpaque(track).takeUnretainedValue()
+    room.localParticipant?.publishVideoTrack(track: track).then { _ in
+        callback(callback_data, UnsafeRawPointer(nil) as! CFString?)
+    }.catch { error in
+        callback(callback_data, error.localizedDescription as CFString)
+    }
+}
+
+@_cdecl("LKCreateScreenShareTrackForWindow")
+public func LKCreateScreenShareTrackForWindow(windowId: uint32) -> UnsafeMutableRawPointer {
+    let track = LocalVideoTrack.createMacOSScreenShareTrack(source: .window(id: windowId))
+    return Unmanaged.passRetained(track).toOpaque()
+}
+
+@_cdecl("LKVideoRendererCreate")
+public func LKVideoRendererCreate(data: UnsafeRawPointer, onFrame: @escaping @convention(c) (UnsafeRawPointer, CVPixelBuffer) -> Void, onDrop: @escaping @convention(c) (UnsafeRawPointer) -> Void) -> UnsafeMutableRawPointer {
+    Unmanaged.passRetained(LKVideoRenderer(data: data, onFrame: onFrame, onDrop: onDrop)).toOpaque()
+}
+
+@_cdecl("LKVideoTrackAddRenderer")
+public func LKVideoTrackAddRenderer(track: UnsafeRawPointer, renderer: UnsafeRawPointer) {
+    let track = Unmanaged<Track>.fromOpaque(track).takeUnretainedValue() as! VideoTrack
+    let renderer = Unmanaged<LKVideoRenderer>.fromOpaque(renderer).takeRetainedValue()
+    track.add(videoRenderer: renderer)
+}

crates/live_kit/build.rs 🔗

@@ -0,0 +1,148 @@
+use serde::Deserialize;
+use std::{
+    env,
+    path::{Path, PathBuf},
+    process::Command,
+};
+
+const SWIFT_PACKAGE_NAME: &'static str = "LiveKitBridge";
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct SwiftTargetInfo {
+    pub triple: String,
+    pub unversioned_triple: String,
+    pub module_triple: String,
+    pub swift_runtime_compatibility_version: String,
+    #[serde(rename = "librariesRequireRPath")]
+    pub libraries_require_rpath: bool,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct SwiftPaths {
+    pub runtime_library_paths: Vec<String>,
+    pub runtime_library_import_paths: Vec<String>,
+    pub runtime_resource_path: String,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct SwiftTarget {
+    pub target: SwiftTargetInfo,
+    pub paths: SwiftPaths,
+}
+
+const MACOS_TARGET_VERSION: &str = "10.15";
+
+fn main() {
+    let swift_target = get_swift_target();
+
+    build_bridge(&swift_target);
+    link_swift_stdlib(&swift_target);
+    link_webrtc_framework(&swift_target);
+}
+
+fn build_bridge(swift_target: &SwiftTarget) {
+    println!("cargo:rerun-if-changed={}/Sources", SWIFT_PACKAGE_NAME);
+    println!(
+        "cargo:rerun-if-changed={}/Package.swift",
+        SWIFT_PACKAGE_NAME
+    );
+    println!(
+        "cargo:rerun-if-changed={}/Package.resolved",
+        SWIFT_PACKAGE_NAME
+    );
+    let swift_package_root = swift_package_root();
+    if !Command::new("swift")
+        .arg("build")
+        .args(&["--configuration", &env::var("PROFILE").unwrap()])
+        .args(&["--triple", &swift_target.target.triple])
+        .current_dir(&swift_package_root)
+        .status()
+        .unwrap()
+        .success()
+    {
+        panic!(
+            "Failed to compile swift package in {}",
+            swift_package_root.display()
+        );
+    }
+
+    println!(
+        "cargo:rustc-link-search=native={}",
+        swift_target.out_dir_path().display()
+    );
+    println!("cargo:rustc-link-lib=static={}", SWIFT_PACKAGE_NAME);
+}
+
+fn link_swift_stdlib(swift_target: &SwiftTarget) {
+    swift_target
+        .paths
+        .runtime_library_paths
+        .iter()
+        .for_each(|path| {
+            println!("cargo:rustc-link-search=native={}", path);
+        });
+}
+
+fn link_webrtc_framework(swift_target: &SwiftTarget) {
+    let swift_out_dir_path = swift_target.out_dir_path();
+    println!("cargo:rustc-link-lib=framework=WebRTC");
+    println!(
+        "cargo:rustc-link-search=framework={}",
+        swift_out_dir_path.display()
+    );
+    // Find WebRTC.framework as a sibling of the executable when running tests.
+    println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path");
+
+    let source_path = swift_out_dir_path.join("WebRTC.framework");
+    let deps_dir_path =
+        PathBuf::from(env::var("OUT_DIR").unwrap()).join("../../../deps/WebRTC.framework");
+    let target_dir_path =
+        PathBuf::from(env::var("OUT_DIR").unwrap()).join("../../../WebRTC.framework");
+    copy_dir(&source_path, &deps_dir_path);
+    copy_dir(&source_path, &target_dir_path);
+}
+
+fn get_swift_target() -> SwiftTarget {
+    let mut arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap();
+    if arch == "aarch64" {
+        arch = "arm64".into();
+    }
+    let target = format!("{}-apple-macosx{}", arch, MACOS_TARGET_VERSION);
+
+    let swift_target_info_str = Command::new("swift")
+        .args(&["-target", &target, "-print-target-info"])
+        .output()
+        .unwrap()
+        .stdout;
+
+    serde_json::from_slice(&swift_target_info_str).unwrap()
+}
+
+fn swift_package_root() -> PathBuf {
+    env::current_dir().unwrap().join(SWIFT_PACKAGE_NAME)
+}
+
+fn copy_dir(source: &Path, destination: &Path) {
+    assert!(
+        Command::new("cp")
+            .arg("-r")
+            .args(&[source, destination])
+            .status()
+            .unwrap()
+            .success(),
+        "could not copy {:?} to {:?}",
+        source,
+        destination
+    );
+}
+
+impl SwiftTarget {
+    fn out_dir_path(&self) -> PathBuf {
+        swift_package_root()
+            .join(".build")
+            .join(&self.target.unversioned_triple)
+            .join(env::var("PROFILE").unwrap())
+    }
+}

crates/live_kit/src/live_kit.rs 🔗

@@ -0,0 +1,276 @@
+use anyhow::{anyhow, Context, Result};
+use core_foundation::{
+    array::CFArray,
+    base::{TCFType, TCFTypeRef},
+    dictionary::CFDictionary,
+    number::CFNumber,
+    string::{CFString, CFStringRef},
+};
+use core_graphics::window::{
+    kCGNullWindowID, kCGWindowListOptionExcludeDesktopElements, kCGWindowListOptionOnScreenOnly,
+    kCGWindowNumber, kCGWindowOwnerName, kCGWindowOwnerPID, CGWindowListCopyWindowInfo,
+};
+use futures::{
+    channel::{mpsc, oneshot},
+    Future,
+};
+use media::core_video::{CVImageBuffer, CVImageBufferRef};
+use parking_lot::Mutex;
+use std::{
+    ffi::c_void,
+    sync::{Arc, Weak},
+};
+
+extern "C" {
+    fn LKRelease(object: *const c_void);
+
+    fn LKRoomDelegateCreate(
+        callback_data: *mut c_void,
+        on_did_subscribe_to_remote_video_track: extern "C" fn(
+            callback_data: *mut c_void,
+            remote_track: *const c_void,
+        ),
+    ) -> *const c_void;
+
+    fn LKRoomCreate(delegate: *const c_void) -> *const c_void;
+    fn LKRoomConnect(
+        room: *const c_void,
+        url: CFStringRef,
+        token: CFStringRef,
+        callback: extern "C" fn(*mut c_void, CFStringRef),
+        callback_data: *mut c_void,
+    );
+    fn LKRoomPublishVideoTrack(
+        room: *const c_void,
+        track: *const c_void,
+        callback: extern "C" fn(*mut c_void, CFStringRef),
+        callback_data: *mut c_void,
+    );
+
+    fn LKVideoRendererCreate(
+        callback_data: *mut c_void,
+        on_frame: extern "C" fn(callback_data: *mut c_void, frame: CVImageBufferRef),
+        on_drop: extern "C" fn(callback_data: *mut c_void),
+    ) -> *const c_void;
+
+    fn LKVideoTrackAddRenderer(track: *const c_void, renderer: *const c_void);
+
+    fn LKCreateScreenShareTrackForWindow(windowId: u32) -> *const c_void;
+}
+
+pub struct Room {
+    native_room: *const c_void,
+    remote_video_track_subscribers: Mutex<Vec<mpsc::UnboundedSender<Arc<RemoteVideoTrack>>>>,
+    _delegate: RoomDelegate,
+}
+
+impl Room {
+    pub fn new() -> Arc<Self> {
+        Arc::new_cyclic(|weak_room| {
+            let delegate = RoomDelegate::new(weak_room.clone());
+            Self {
+                native_room: unsafe { LKRoomCreate(delegate.native_delegate) },
+                remote_video_track_subscribers: Default::default(),
+                _delegate: delegate,
+            }
+        })
+    }
+
+    pub fn connect(&self, url: &str, token: &str) -> impl Future<Output = Result<()>> {
+        let url = CFString::new(url);
+        let token = CFString::new(token);
+        let (did_connect, tx, rx) = Self::build_done_callback();
+        unsafe {
+            LKRoomConnect(
+                self.native_room,
+                url.as_concrete_TypeRef(),
+                token.as_concrete_TypeRef(),
+                did_connect,
+                tx,
+            )
+        }
+
+        async { rx.await.unwrap().context("error connecting to room") }
+    }
+
+    pub fn publish_video_track(&self, track: &LocalVideoTrack) -> impl Future<Output = Result<()>> {
+        let (did_publish, tx, rx) = Self::build_done_callback();
+        unsafe {
+            LKRoomPublishVideoTrack(self.native_room, track.0, did_publish, tx);
+        }
+        async { rx.await.unwrap().context("error publishing video track") }
+    }
+
+    pub fn remote_video_tracks(&self) -> mpsc::UnboundedReceiver<Arc<RemoteVideoTrack>> {
+        let (tx, rx) = mpsc::unbounded();
+        self.remote_video_track_subscribers.lock().push(tx);
+        rx
+    }
+
+    fn did_subscribe_to_remote_video_track(&self, track: RemoteVideoTrack) {
+        let track = Arc::new(track);
+        self.remote_video_track_subscribers
+            .lock()
+            .retain(|tx| tx.unbounded_send(track.clone()).is_ok());
+    }
+
+    fn build_done_callback() -> (
+        extern "C" fn(*mut c_void, CFStringRef),
+        *mut c_void,
+        oneshot::Receiver<Result<()>>,
+    ) {
+        let (tx, rx) = oneshot::channel();
+        extern "C" fn done_callback(tx: *mut c_void, error: CFStringRef) {
+            let tx = unsafe { Box::from_raw(tx as *mut oneshot::Sender<Result<()>>) };
+            if error.is_null() {
+                let _ = tx.send(Ok(()));
+            } else {
+                let error = unsafe { CFString::wrap_under_get_rule(error).to_string() };
+                let _ = tx.send(Err(anyhow!(error)));
+            }
+        }
+        (
+            done_callback,
+            Box::into_raw(Box::new(tx)) as *mut c_void,
+            rx,
+        )
+    }
+}
+
+impl Drop for Room {
+    fn drop(&mut self) {
+        unsafe { LKRelease(self.native_room) }
+    }
+}
+
+struct RoomDelegate {
+    native_delegate: *const c_void,
+    weak_room: *const Room,
+}
+
+impl RoomDelegate {
+    fn new(weak_room: Weak<Room>) -> Self {
+        let weak_room = Weak::into_raw(weak_room);
+        let native_delegate = unsafe {
+            LKRoomDelegateCreate(
+                weak_room as *mut c_void,
+                Self::on_did_subscribe_to_remote_video_track,
+            )
+        };
+        Self {
+            native_delegate,
+            weak_room,
+        }
+    }
+
+    extern "C" fn on_did_subscribe_to_remote_video_track(room: *mut c_void, track: *const c_void) {
+        let room = unsafe { Weak::from_raw(room as *mut Room) };
+        let track = RemoteVideoTrack(track);
+        if let Some(room) = room.upgrade() {
+            room.did_subscribe_to_remote_video_track(track);
+        }
+        let _ = Weak::into_raw(room);
+    }
+}
+
+impl Drop for RoomDelegate {
+    fn drop(&mut self) {
+        unsafe {
+            LKRelease(self.native_delegate);
+            let _ = Weak::from_raw(self.weak_room);
+        }
+    }
+}
+
+pub struct LocalVideoTrack(*const c_void);
+
+impl LocalVideoTrack {
+    pub fn screen_share_for_window(window_id: u32) -> Self {
+        Self(unsafe { LKCreateScreenShareTrackForWindow(window_id) })
+    }
+}
+
+impl Drop for LocalVideoTrack {
+    fn drop(&mut self) {
+        unsafe { LKRelease(self.0) }
+    }
+}
+
+pub struct RemoteVideoTrack(*const c_void);
+
+impl RemoteVideoTrack {
+    pub fn add_renderer<F>(&self, callback: F)
+    where
+        F: 'static + FnMut(CVImageBuffer),
+    {
+        extern "C" fn on_frame<F>(callback_data: *mut c_void, frame: CVImageBufferRef)
+        where
+            F: FnMut(CVImageBuffer),
+        {
+            unsafe {
+                let buffer = CVImageBuffer::wrap_under_get_rule(frame);
+                let callback = &mut *(callback_data as *mut F);
+                callback(buffer);
+            }
+        }
+
+        extern "C" fn on_drop<F>(callback_data: *mut c_void) {
+            unsafe {
+                let _ = Box::from_raw(callback_data as *mut F);
+            }
+        }
+
+        let callback_data = Box::into_raw(Box::new(callback));
+        unsafe {
+            let renderer =
+                LKVideoRendererCreate(callback_data as *mut c_void, on_frame::<F>, on_drop::<F>);
+            LKVideoTrackAddRenderer(self.0, renderer);
+        }
+    }
+}
+
+impl Drop for RemoteVideoTrack {
+    fn drop(&mut self) {
+        unsafe { LKRelease(self.0) }
+    }
+}
+
+#[derive(Debug)]
+pub struct WindowInfo {
+    pub id: u32,
+    pub owner_pid: i32,
+    pub owner_name: Option<String>,
+}
+
+pub fn list_windows() -> Vec<WindowInfo> {
+    unsafe {
+        let dicts = CFArray::<CFDictionary>::wrap_under_get_rule(CGWindowListCopyWindowInfo(
+            kCGWindowListOptionOnScreenOnly | kCGWindowListOptionExcludeDesktopElements,
+            kCGNullWindowID,
+        ));
+
+        dicts
+            .iter()
+            .map(|dict| {
+                let id =
+                    CFNumber::wrap_under_get_rule(*dict.get(kCGWindowNumber.as_void_ptr()) as _)
+                        .to_i64()
+                        .unwrap() as u32;
+
+                let owner_pid =
+                    CFNumber::wrap_under_get_rule(*dict.get(kCGWindowOwnerPID.as_void_ptr()) as _)
+                        .to_i32()
+                        .unwrap();
+
+                let owner_name = dict
+                    .find(kCGWindowOwnerName.as_void_ptr())
+                    .map(|name| CFString::wrap_under_get_rule(*name as _).to_string());
+                WindowInfo {
+                    id,
+                    owner_pid,
+                    owner_name,
+                }
+            })
+            .collect()
+    }
+}

crates/media/Cargo.toml 🔗

@@ -0,0 +1,20 @@
+[package]
+name = "media"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+path = "src/media.rs"
+doctest = false
+
+[dependencies]
+anyhow = "1.0"
+block = "0.1"
+bytes = "1.2"
+core-foundation = "0.9.3"
+foreign-types = "0.3"
+metal = "0.21.0"
+objc = "0.2"
+
+[build-dependencies]
+bindgen = "0.59.2"

crates/media/build.rs 🔗

@@ -0,0 +1,39 @@
+use std::{env, path::PathBuf, process::Command};
+
+fn main() {
+    let sdk_path = String::from_utf8(
+        Command::new("xcrun")
+            .args(&["--sdk", "macosx", "--show-sdk-path"])
+            .output()
+            .unwrap()
+            .stdout,
+    )
+    .unwrap();
+    let sdk_path = sdk_path.trim_end();
+
+    println!("cargo:rerun-if-changed=src/bindings.h");
+    let bindings = bindgen::Builder::default()
+        .header("src/bindings.h")
+        .clang_arg(format!("-isysroot{}", sdk_path))
+        .clang_arg("-xobjective-c")
+        .allowlist_type("CMItemIndex")
+        .allowlist_type("CMSampleTimingInfo")
+        .allowlist_type("CMVideoCodecType")
+        .allowlist_type("VTEncodeInfoFlags")
+        .allowlist_function("CMTimeMake")
+        .allowlist_var("kCVPixelFormatType_.*")
+        .allowlist_var("kCVReturn.*")
+        .allowlist_var("VTEncodeInfoFlags_.*")
+        .allowlist_var("kCMVideoCodecType_.*")
+        .allowlist_var("kCMTime.*")
+        .allowlist_var("kCMSampleAttachmentKey_.*")
+        .parse_callbacks(Box::new(bindgen::CargoCallbacks))
+        .layout_tests(false)
+        .generate()
+        .expect("unable to generate bindings");
+
+    let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
+    bindings
+        .write_to_file(out_path.join("bindings.rs"))
+        .expect("couldn't write dispatch bindings");
+}

crates/media/src/bindings.h 🔗

@@ -0,0 +1,5 @@
+#import <CoreMedia/CMFormatDescription.h>
+#import <CoreMedia/CMSampleBuffer.h>
+#import <CoreVideo/CVPixelFormatDescription.h>
+#import <CoreVideo/CVReturn.h>
+#import <VideoToolbox/VTCompressionSession.h>

crates/media/src/bindings.rs 🔗

@@ -0,0 +1,8 @@
+#![allow(non_upper_case_globals)]
+#![allow(non_camel_case_types)]
+#![allow(non_snake_case)]
+#![allow(unused)]
+
+use objc::*;
+
+include!(concat!(env!("OUT_DIR"), "/bindings.rs"));

crates/media/src/media.rs 🔗

@@ -0,0 +1,534 @@
+#![allow(non_snake_case)]
+#![allow(non_camel_case_types)]
+
+mod bindings;
+
+use core_foundation::{
+    base::{CFTypeID, TCFType},
+    declare_TCFType, impl_CFTypeDescription, impl_TCFType,
+};
+use std::ffi::c_void;
+
+pub mod io_surface {
+    use super::*;
+
+    #[repr(C)]
+    pub struct __IOSurface(c_void);
+    // The ref type must be a pointer to the underlying struct.
+    pub type IOSurfaceRef = *const __IOSurface;
+
+    declare_TCFType!(IOSurface, IOSurfaceRef);
+    impl_TCFType!(IOSurface, IOSurfaceRef, IOSurfaceGetTypeID);
+    impl_CFTypeDescription!(IOSurface);
+
+    #[link(name = "IOSurface", kind = "framework")]
+    extern "C" {
+        fn IOSurfaceGetTypeID() -> CFTypeID;
+    }
+}
+
+pub mod core_video {
+    #![allow(non_snake_case)]
+
+    use super::*;
+    pub use crate::bindings::{
+        kCVPixelFormatType_32BGRA, kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
+        kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, kCVPixelFormatType_420YpCbCr8Planar,
+    };
+    use crate::bindings::{kCVReturnSuccess, CVReturn, OSType};
+    use anyhow::{anyhow, Result};
+    use core_foundation::{
+        base::kCFAllocatorDefault, dictionary::CFDictionaryRef, mach_port::CFAllocatorRef,
+    };
+    use foreign_types::ForeignTypeRef;
+    use io_surface::{IOSurface, IOSurfaceRef};
+    use metal::{MTLDevice, MTLPixelFormat};
+    use std::ptr;
+
+    #[repr(C)]
+    pub struct __CVImageBuffer(c_void);
+    // The ref type must be a pointer to the underlying struct.
+    pub type CVImageBufferRef = *const __CVImageBuffer;
+
+    declare_TCFType!(CVImageBuffer, CVImageBufferRef);
+    impl_TCFType!(CVImageBuffer, CVImageBufferRef, CVImageBufferGetTypeID);
+    impl_CFTypeDescription!(CVImageBuffer);
+
+    impl CVImageBuffer {
+        pub fn io_surface(&self) -> IOSurface {
+            unsafe {
+                IOSurface::wrap_under_get_rule(CVPixelBufferGetIOSurface(
+                    self.as_concrete_TypeRef(),
+                ))
+            }
+        }
+
+        pub fn width(&self) -> usize {
+            unsafe { CVPixelBufferGetWidth(self.as_concrete_TypeRef()) }
+        }
+
+        pub fn height(&self) -> usize {
+            unsafe { CVPixelBufferGetHeight(self.as_concrete_TypeRef()) }
+        }
+
+        pub fn plane_width(&self, plane: usize) -> usize {
+            unsafe { CVPixelBufferGetWidthOfPlane(self.as_concrete_TypeRef(), plane) }
+        }
+
+        pub fn plane_height(&self, plane: usize) -> usize {
+            unsafe { CVPixelBufferGetHeightOfPlane(self.as_concrete_TypeRef(), plane) }
+        }
+
+        pub fn pixel_format_type(&self) -> OSType {
+            unsafe { CVPixelBufferGetPixelFormatType(self.as_concrete_TypeRef()) }
+        }
+    }
+
+    #[link(name = "CoreVideo", kind = "framework")]
+    extern "C" {
+        fn CVImageBufferGetTypeID() -> CFTypeID;
+        fn CVPixelBufferGetIOSurface(buffer: CVImageBufferRef) -> IOSurfaceRef;
+        fn CVPixelBufferGetWidth(buffer: CVImageBufferRef) -> usize;
+        fn CVPixelBufferGetHeight(buffer: CVImageBufferRef) -> usize;
+        fn CVPixelBufferGetWidthOfPlane(buffer: CVImageBufferRef, plane: usize) -> usize;
+        fn CVPixelBufferGetHeightOfPlane(buffer: CVImageBufferRef, plane: usize) -> usize;
+        fn CVPixelBufferGetPixelFormatType(buffer: CVImageBufferRef) -> OSType;
+    }
+
+    #[repr(C)]
+    pub struct __CVMetalTextureCache(c_void);
+    pub type CVMetalTextureCacheRef = *const __CVMetalTextureCache;
+
+    declare_TCFType!(CVMetalTextureCache, CVMetalTextureCacheRef);
+    impl_TCFType!(
+        CVMetalTextureCache,
+        CVMetalTextureCacheRef,
+        CVMetalTextureCacheGetTypeID
+    );
+    impl_CFTypeDescription!(CVMetalTextureCache);
+
+    impl CVMetalTextureCache {
+        pub fn new(metal_device: *mut MTLDevice) -> Result<Self> {
+            unsafe {
+                let mut this = ptr::null();
+                let result = CVMetalTextureCacheCreate(
+                    kCFAllocatorDefault,
+                    ptr::null_mut(),
+                    metal_device,
+                    ptr::null_mut(),
+                    &mut this,
+                );
+                if result == kCVReturnSuccess {
+                    Ok(CVMetalTextureCache::wrap_under_create_rule(this))
+                } else {
+                    Err(anyhow!("could not create texture cache, code: {}", result))
+                }
+            }
+        }
+
+        pub fn create_texture_from_image(
+            &self,
+            source: CVImageBufferRef,
+            texture_attributes: CFDictionaryRef,
+            pixel_format: MTLPixelFormat,
+            width: usize,
+            height: usize,
+            plane_index: usize,
+        ) -> Result<CVMetalTexture> {
+            unsafe {
+                let mut this = ptr::null();
+                let result = CVMetalTextureCacheCreateTextureFromImage(
+                    kCFAllocatorDefault,
+                    self.as_concrete_TypeRef(),
+                    source,
+                    texture_attributes,
+                    pixel_format,
+                    width,
+                    height,
+                    plane_index,
+                    &mut this,
+                );
+                if result == kCVReturnSuccess {
+                    Ok(CVMetalTexture::wrap_under_create_rule(this))
+                } else {
+                    Err(anyhow!("could not create texture, code: {}", result))
+                }
+            }
+        }
+    }
+
+    #[link(name = "CoreVideo", kind = "framework")]
+    extern "C" {
+        fn CVMetalTextureCacheGetTypeID() -> CFTypeID;
+        fn CVMetalTextureCacheCreate(
+            allocator: CFAllocatorRef,
+            cache_attributes: CFDictionaryRef,
+            metal_device: *const MTLDevice,
+            texture_attributes: CFDictionaryRef,
+            cache_out: *mut CVMetalTextureCacheRef,
+        ) -> CVReturn;
+        fn CVMetalTextureCacheCreateTextureFromImage(
+            allocator: CFAllocatorRef,
+            texture_cache: CVMetalTextureCacheRef,
+            source_image: CVImageBufferRef,
+            texture_attributes: CFDictionaryRef,
+            pixel_format: MTLPixelFormat,
+            width: usize,
+            height: usize,
+            plane_index: usize,
+            texture_out: *mut CVMetalTextureRef,
+        ) -> CVReturn;
+    }
+
+    #[repr(C)]
+    pub struct __CVMetalTexture(c_void);
+    pub type CVMetalTextureRef = *const __CVMetalTexture;
+
+    declare_TCFType!(CVMetalTexture, CVMetalTextureRef);
+    impl_TCFType!(CVMetalTexture, CVMetalTextureRef, CVMetalTextureGetTypeID);
+    impl_CFTypeDescription!(CVMetalTexture);
+
+    impl CVMetalTexture {
+        pub fn as_texture_ref(&self) -> &metal::TextureRef {
+            unsafe {
+                let texture = CVMetalTextureGetTexture(self.as_concrete_TypeRef());
+                &metal::TextureRef::from_ptr(texture as *mut _)
+            }
+        }
+    }
+
+    #[link(name = "CoreVideo", kind = "framework")]
+    extern "C" {
+        fn CVMetalTextureGetTypeID() -> CFTypeID;
+        fn CVMetalTextureGetTexture(texture: CVMetalTextureRef) -> *mut c_void;
+    }
+}
+
+pub mod core_media {
+    #![allow(non_snake_case)]
+
+    pub use crate::bindings::{
+        kCMSampleAttachmentKey_NotSync, kCMTimeInvalid, kCMVideoCodecType_H264, CMItemIndex,
+        CMSampleTimingInfo, CMTime, CMTimeMake, CMVideoCodecType,
+    };
+    use crate::core_video::{CVImageBuffer, CVImageBufferRef};
+    use anyhow::{anyhow, Result};
+    use core_foundation::{
+        array::{CFArray, CFArrayRef},
+        base::{CFTypeID, OSStatus, TCFType},
+        declare_TCFType,
+        dictionary::CFDictionary,
+        impl_CFTypeDescription, impl_TCFType,
+        string::CFString,
+    };
+    use std::{ffi::c_void, ptr};
+
+    #[repr(C)]
+    pub struct __CMSampleBuffer(c_void);
+    // The ref type must be a pointer to the underlying struct.
+    pub type CMSampleBufferRef = *const __CMSampleBuffer;
+
+    declare_TCFType!(CMSampleBuffer, CMSampleBufferRef);
+    impl_TCFType!(CMSampleBuffer, CMSampleBufferRef, CMSampleBufferGetTypeID);
+    impl_CFTypeDescription!(CMSampleBuffer);
+
+    impl CMSampleBuffer {
+        pub fn attachments(&self) -> Vec<CFDictionary<CFString>> {
+            unsafe {
+                let attachments =
+                    CMSampleBufferGetSampleAttachmentsArray(self.as_concrete_TypeRef(), true);
+                CFArray::<CFDictionary>::wrap_under_get_rule(attachments)
+                    .into_iter()
+                    .map(|attachments| {
+                        CFDictionary::wrap_under_get_rule(attachments.as_concrete_TypeRef())
+                    })
+                    .collect()
+            }
+        }
+
+        pub fn image_buffer(&self) -> CVImageBuffer {
+            unsafe {
+                CVImageBuffer::wrap_under_get_rule(CMSampleBufferGetImageBuffer(
+                    self.as_concrete_TypeRef(),
+                ))
+            }
+        }
+
+        pub fn sample_timing_info(&self, index: usize) -> Result<CMSampleTimingInfo> {
+            unsafe {
+                let mut timing_info = CMSampleTimingInfo {
+                    duration: kCMTimeInvalid,
+                    presentationTimeStamp: kCMTimeInvalid,
+                    decodeTimeStamp: kCMTimeInvalid,
+                };
+                let result = CMSampleBufferGetSampleTimingInfo(
+                    self.as_concrete_TypeRef(),
+                    index as CMItemIndex,
+                    &mut timing_info,
+                );
+
+                if result == 0 {
+                    Ok(timing_info)
+                } else {
+                    Err(anyhow!("error getting sample timing info, code {}", result))
+                }
+            }
+        }
+
+        pub fn format_description(&self) -> CMFormatDescription {
+            unsafe {
+                CMFormatDescription::wrap_under_get_rule(CMSampleBufferGetFormatDescription(
+                    self.as_concrete_TypeRef(),
+                ))
+            }
+        }
+
+        pub fn data(&self) -> CMBlockBuffer {
+            unsafe {
+                CMBlockBuffer::wrap_under_get_rule(CMSampleBufferGetDataBuffer(
+                    self.as_concrete_TypeRef(),
+                ))
+            }
+        }
+    }
+
+    #[link(name = "CoreMedia", kind = "framework")]
+    extern "C" {
+        fn CMSampleBufferGetTypeID() -> CFTypeID;
+        fn CMSampleBufferGetSampleAttachmentsArray(
+            buffer: CMSampleBufferRef,
+            create_if_necessary: bool,
+        ) -> CFArrayRef;
+        fn CMSampleBufferGetImageBuffer(buffer: CMSampleBufferRef) -> CVImageBufferRef;
+        fn CMSampleBufferGetSampleTimingInfo(
+            buffer: CMSampleBufferRef,
+            index: CMItemIndex,
+            timing_info_out: *mut CMSampleTimingInfo,
+        ) -> OSStatus;
+        fn CMSampleBufferGetFormatDescription(buffer: CMSampleBufferRef) -> CMFormatDescriptionRef;
+        fn CMSampleBufferGetDataBuffer(sample_buffer: CMSampleBufferRef) -> CMBlockBufferRef;
+    }
+
+    #[repr(C)]
+    pub struct __CMFormatDescription(c_void);
+    pub type CMFormatDescriptionRef = *const __CMFormatDescription;
+
+    declare_TCFType!(CMFormatDescription, CMFormatDescriptionRef);
+    impl_TCFType!(
+        CMFormatDescription,
+        CMFormatDescriptionRef,
+        CMFormatDescriptionGetTypeID
+    );
+    impl_CFTypeDescription!(CMFormatDescription);
+
+    impl CMFormatDescription {
+        pub fn h264_parameter_set_count(&self) -> usize {
+            unsafe {
+                let mut count = 0;
+                let result = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(
+                    self.as_concrete_TypeRef(),
+                    0,
+                    ptr::null_mut(),
+                    ptr::null_mut(),
+                    &mut count,
+                    ptr::null_mut(),
+                );
+                assert_eq!(result, 0);
+                count
+            }
+        }
+
+        pub fn h264_parameter_set_at_index(&self, index: usize) -> Result<&[u8]> {
+            unsafe {
+                let mut bytes = ptr::null();
+                let mut len = 0;
+                let result = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(
+                    self.as_concrete_TypeRef(),
+                    index,
+                    &mut bytes,
+                    &mut len,
+                    ptr::null_mut(),
+                    ptr::null_mut(),
+                );
+                if result == 0 {
+                    Ok(std::slice::from_raw_parts(bytes, len))
+                } else {
+                    Err(anyhow!("error getting parameter set, code: {}", result))
+                }
+            }
+        }
+    }
+
+    #[link(name = "CoreMedia", kind = "framework")]
+    extern "C" {
+        fn CMFormatDescriptionGetTypeID() -> CFTypeID;
+        fn CMVideoFormatDescriptionGetH264ParameterSetAtIndex(
+            video_desc: CMFormatDescriptionRef,
+            parameter_set_index: usize,
+            parameter_set_pointer_out: *mut *const u8,
+            parameter_set_size_out: *mut usize,
+            parameter_set_count_out: *mut usize,
+            NALUnitHeaderLengthOut: *mut isize,
+        ) -> OSStatus;
+    }
+
+    #[repr(C)]
+    pub struct __CMBlockBuffer(c_void);
+    pub type CMBlockBufferRef = *const __CMBlockBuffer;
+
+    declare_TCFType!(CMBlockBuffer, CMBlockBufferRef);
+    impl_TCFType!(CMBlockBuffer, CMBlockBufferRef, CMBlockBufferGetTypeID);
+    impl_CFTypeDescription!(CMBlockBuffer);
+
+    impl CMBlockBuffer {
+        pub fn bytes(&self) -> &[u8] {
+            unsafe {
+                let mut bytes = ptr::null();
+                let mut len = 0;
+                let result = CMBlockBufferGetDataPointer(
+                    self.as_concrete_TypeRef(),
+                    0,
+                    &mut 0,
+                    &mut len,
+                    &mut bytes,
+                );
+                assert!(result == 0, "could not get block buffer data");
+                std::slice::from_raw_parts(bytes, len)
+            }
+        }
+    }
+
+    #[link(name = "CoreMedia", kind = "framework")]
+    extern "C" {
+        fn CMBlockBufferGetTypeID() -> CFTypeID;
+        fn CMBlockBufferGetDataPointer(
+            buffer: CMBlockBufferRef,
+            offset: usize,
+            length_at_offset_out: *mut usize,
+            total_length_out: *mut usize,
+            data_pointer_out: *mut *const u8,
+        ) -> OSStatus;
+    }
+}
+
+pub mod video_toolbox {
+    #![allow(non_snake_case)]
+
+    use super::*;
+    use crate::{
+        core_media::{CMSampleBufferRef, CMTime, CMVideoCodecType},
+        core_video::CVImageBufferRef,
+    };
+    use anyhow::{anyhow, Result};
+    pub use bindings::VTEncodeInfoFlags;
+    use core_foundation::{base::OSStatus, dictionary::CFDictionaryRef, mach_port::CFAllocatorRef};
+    use std::ptr;
+
+    #[repr(C)]
+    pub struct __VTCompressionSession(c_void);
+    // The ref type must be a pointer to the underlying struct.
+    pub type VTCompressionSessionRef = *const __VTCompressionSession;
+
+    declare_TCFType!(VTCompressionSession, VTCompressionSessionRef);
+    impl_TCFType!(
+        VTCompressionSession,
+        VTCompressionSessionRef,
+        VTCompressionSessionGetTypeID
+    );
+    impl_CFTypeDescription!(VTCompressionSession);
+
+    impl VTCompressionSession {
+        pub fn new(
+            width: usize,
+            height: usize,
+            codec: CMVideoCodecType,
+            callback: VTCompressionOutputCallback,
+            callback_data: *const c_void,
+        ) -> Result<Self> {
+            unsafe {
+                let mut this = ptr::null();
+                let result = VTCompressionSessionCreate(
+                    ptr::null(),
+                    width as i32,
+                    height as i32,
+                    codec,
+                    ptr::null(),
+                    ptr::null(),
+                    ptr::null(),
+                    callback,
+                    callback_data,
+                    &mut this,
+                );
+
+                if result == 0 {
+                    Ok(Self::wrap_under_create_rule(this))
+                } else {
+                    Err(anyhow!(
+                        "error creating compression session, code {}",
+                        result
+                    ))
+                }
+            }
+        }
+
+        pub fn encode_frame(
+            &self,
+            buffer: CVImageBufferRef,
+            presentation_timestamp: CMTime,
+            duration: CMTime,
+        ) -> Result<()> {
+            unsafe {
+                let result = VTCompressionSessionEncodeFrame(
+                    self.as_concrete_TypeRef(),
+                    buffer,
+                    presentation_timestamp,
+                    duration,
+                    ptr::null(),
+                    ptr::null(),
+                    ptr::null_mut(),
+                );
+                if result == 0 {
+                    Ok(())
+                } else {
+                    Err(anyhow!("error encoding frame, code {}", result))
+                }
+            }
+        }
+    }
+
+    type VTCompressionOutputCallback = Option<
+        unsafe extern "C" fn(
+            outputCallbackRefCon: *mut c_void,
+            sourceFrameRefCon: *mut c_void,
+            status: OSStatus,
+            infoFlags: VTEncodeInfoFlags,
+            sampleBuffer: CMSampleBufferRef,
+        ),
+    >;
+
+    #[link(name = "VideoToolbox", kind = "framework")]
+    extern "C" {
+        fn VTCompressionSessionGetTypeID() -> CFTypeID;
+        fn VTCompressionSessionCreate(
+            allocator: CFAllocatorRef,
+            width: i32,
+            height: i32,
+            codec_type: CMVideoCodecType,
+            encoder_specification: CFDictionaryRef,
+            source_image_buffer_attributes: CFDictionaryRef,
+            compressed_data_allocator: CFAllocatorRef,
+            output_callback: VTCompressionOutputCallback,
+            output_callback_ref_con: *const c_void,
+            compression_session_out: *mut VTCompressionSessionRef,
+        ) -> OSStatus;
+        fn VTCompressionSessionEncodeFrame(
+            session: VTCompressionSessionRef,
+            image_buffer: CVImageBufferRef,
+            presentation_timestamp: CMTime,
+            duration: CMTime,
+            frame_properties: CFDictionaryRef,
+            source_frame_ref_con: *const c_void,
+            output_flags: *mut VTEncodeInfoFlags,
+        ) -> OSStatus;
+    }
+}

crates/picker/src/picker.rs 🔗

@@ -85,7 +85,7 @@ impl<D: PickerDelegate> View for Picker<D> {
                             let selected_ix = delegate.read(cx).selected_index();
                             range.end = cmp::min(range.end, delegate.read(cx).match_count());
                             items.extend(range.map(move |ix| {
-                                MouseEventHandler::new::<D, _, _>(ix, cx, |state, cx| {
+                                MouseEventHandler::<D>::new(ix, cx, |state, cx| {
                                     delegate
                                         .read(cx)
                                         .render_match(ix, state, ix == selected_ix, cx)

crates/project_panel/src/project_panel.rs 🔗

@@ -5,8 +5,8 @@ use gpui::{
     actions,
     anyhow::{anyhow, Result},
     elements::{
-        ChildView, ConstrainedBox, Empty, Flex, Label, MouseEventHandler, ParentElement,
-        ScrollTarget, Stack, Svg, UniformList, UniformListState,
+        AnchorCorner, ChildView, ConstrainedBox, Empty, Flex, Label, MouseEventHandler,
+        ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState,
     },
     geometry::vector::Vector2F,
     impl_internal_actions, keymap,
@@ -302,7 +302,7 @@ impl ProjectPanel {
         }
 
         self.context_menu.update(cx, |menu, cx| {
-            menu.show(action.position, menu_entries, cx);
+            menu.show(action.position, AnchorCorner::TopLeft, menu_entries, cx);
         });
 
         cx.notify();
@@ -1012,7 +1012,7 @@ impl ProjectPanel {
     ) -> ElementBox {
         let kind = details.kind;
         let show_editor = details.is_editing && !details.is_processing;
-        MouseEventHandler::new::<Self, _, _>(entry_id.to_usize(), cx, |state, _| {
+        MouseEventHandler::<Self>::new(entry_id.to_usize(), cx, |state, _| {
             let padding = theme.container.padding.left + details.depth as f32 * theme.indent_width;
             let mut style = theme.entry.style_for(state, details.is_selected).clone();
             if details.is_ignored {
@@ -1107,7 +1107,7 @@ impl View for ProjectPanel {
         let last_worktree_root_id = self.last_worktree_root_id;
         Stack::new()
             .with_child(
-                MouseEventHandler::new::<Tag, _, _>(0, cx, |_, cx| {
+                MouseEventHandler::<Tag>::new(0, cx, |_, cx| {
                     UniformList::new(
                         self.list.clone(),
                         self.visible_entries
@@ -1243,7 +1243,8 @@ mod tests {
         .await;
 
         let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
+        let (_, workspace) =
+            cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
         let panel = workspace.update(cx, |_, cx| ProjectPanel::new(project, cx));
         assert_eq!(
             visible_entries_as_strings(&panel, 0..50, cx),
@@ -1335,7 +1336,8 @@ mod tests {
         .await;
 
         let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
+        let (_, workspace) =
+            cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
         let panel = workspace.update(cx, |_, cx| ProjectPanel::new(project, cx));
 
         select_path(&panel, "root1", cx);

crates/search/src/buffer_search.rs 🔗

@@ -319,7 +319,7 @@ impl BufferSearchBar {
         let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
         let is_active = self.is_search_option_enabled(option);
         Some(
-            MouseEventHandler::new::<Self, _, _>(option as usize, cx, |state, cx| {
+            MouseEventHandler::<Self>::new(option as usize, cx, |state, cx| {
                 let style = &cx
                     .global::<Settings>()
                     .theme
@@ -367,7 +367,7 @@ impl BufferSearchBar {
         let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
 
         enum NavButton {}
-        MouseEventHandler::new::<NavButton, _, _>(direction as usize, cx, |state, cx| {
+        MouseEventHandler::<NavButton>::new(direction as usize, cx, |state, cx| {
             let style = &cx
                 .global::<Settings>()
                 .theme

crates/search/src/project_search.rs 🔗

@@ -176,7 +176,7 @@ impl View for ProjectSearchView {
             } else {
                 "No results"
             };
-            MouseEventHandler::new::<Status, _, _>(0, cx, |_, _| {
+            MouseEventHandler::<Status>::new(0, cx, |_, _| {
                 Label::new(text.to_string(), theme.search.results_status.clone())
                     .aligned()
                     .contained()
@@ -723,7 +723,7 @@ impl ProjectSearchBar {
         let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
 
         enum NavButton {}
-        MouseEventHandler::new::<NavButton, _, _>(direction as usize, cx, |state, cx| {
+        MouseEventHandler::<NavButton>::new(direction as usize, cx, |state, cx| {
             let style = &cx
                 .global::<Settings>()
                 .theme
@@ -758,7 +758,7 @@ impl ProjectSearchBar {
     ) -> ElementBox {
         let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
         let is_active = self.is_option_enabled(option, cx);
-        MouseEventHandler::new::<Self, _, _>(option as usize, cx, |state, cx| {
+        MouseEventHandler::<Self>::new(option as usize, cx, |state, cx| {
             let style = &cx
                 .global::<Settings>()
                 .theme

crates/settings/src/settings.rs 🔗

@@ -29,6 +29,7 @@ pub struct Settings {
     pub show_completions_on_input: bool,
     pub vim_mode: bool,
     pub autosave: Autosave,
+    pub default_dock_anchor: DockAnchor,
     pub editor_defaults: EditorSettings,
     pub editor_overrides: EditorSettings,
     pub terminal_defaults: TerminalSettings,
@@ -151,6 +152,15 @@ pub enum WorkingDirectory {
     Always { directory: String },
 }
 
+#[derive(PartialEq, Eq, Debug, Default, Copy, Clone, Hash, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum DockAnchor {
+    #[default]
+    Bottom,
+    Right,
+    Expanded,
+}
+
 #[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
 pub struct SettingsFileContent {
     pub experiments: Option<FeatureFlags>,
@@ -168,6 +178,8 @@ pub struct SettingsFileContent {
     pub vim_mode: Option<bool>,
     #[serde(default)]
     pub autosave: Option<Autosave>,
+    #[serde(default)]
+    pub default_dock_anchor: Option<DockAnchor>,
     #[serde(flatten)]
     pub editor: EditorSettings,
     #[serde(default)]
@@ -217,6 +229,7 @@ impl Settings {
             projects_online_by_default: defaults.projects_online_by_default.unwrap(),
             vim_mode: defaults.vim_mode.unwrap(),
             autosave: defaults.autosave.unwrap(),
+            default_dock_anchor: defaults.default_dock_anchor.unwrap(),
             editor_defaults: EditorSettings {
                 tab_size: required(defaults.editor.tab_size),
                 hard_tabs: required(defaults.editor.hard_tabs),
@@ -269,6 +282,8 @@ impl Settings {
         merge(&mut self.autosave, data.autosave);
         merge(&mut self.experiments, data.experiments);
         merge(&mut self.staff_mode, data.staff_mode);
+        merge(&mut self.default_dock_anchor, data.default_dock_anchor);
+
         // Ensure terminal font is loaded, so we can request it in terminal_element layout
         if let Some(terminal_font) = &data.terminal.font_family {
             font_cache.load_family(&[terminal_font]).log_err();
@@ -337,6 +352,7 @@ impl Settings {
             show_completions_on_input: true,
             vim_mode: false,
             autosave: Autosave::Off,
+            default_dock_anchor: DockAnchor::Bottom,
             editor_defaults: EditorSettings {
                 tab_size: Some(4.try_into().unwrap()),
                 hard_tabs: Some(false),

crates/terminal/src/mappings/mouse.rs 🔗

@@ -6,6 +6,7 @@ use alacritty_terminal::grid::Dimensions;
 /// with modifications for our circumstances
 use alacritty_terminal::index::{Column as GridCol, Line as GridLine, Point, Side};
 use alacritty_terminal::term::TermMode;
+use gpui::scene::ScrollWheelRegionEvent;
 use gpui::{geometry::vector::Vector2F, MouseButtonEvent, MouseMovedEvent, ScrollWheelEvent};
 
 use crate::TerminalSize;
@@ -114,7 +115,7 @@ impl MouseButton {
 pub fn scroll_report(
     point: Point,
     scroll_lines: i32,
-    e: &ScrollWheelEvent,
+    e: &ScrollWheelRegionEvent,
     mode: TermMode,
 ) -> Option<impl Iterator<Item = Vec<u8>>> {
     if mode.intersects(TermMode::MOUSE_MODE) {

crates/terminal/src/modal.rs 🔗

@@ -1,78 +0,0 @@
-use gpui::{ModelHandle, ViewContext};
-use settings::{Settings, WorkingDirectory};
-use workspace::{programs::ProgramManager, Workspace};
-
-use crate::{
-    terminal_container_view::{
-        get_working_directory, DeployModal, TerminalContainer, TerminalContainerContent,
-    },
-    Event, Terminal,
-};
-
-pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewContext<Workspace>) {
-    let window = cx.window_id();
-
-    // Pull the terminal connection out of the global if it has been stored
-    let possible_terminal = ProgramManager::remove::<Terminal, _>(window, cx);
-
-    if let Some(terminal_handle) = possible_terminal {
-        workspace.toggle_modal(cx, |_, cx| {
-            // Create a view from the stored connection if the terminal modal is not already shown
-            cx.add_view(|cx| TerminalContainer::from_terminal(terminal_handle.clone(), true, cx))
-        });
-        // Toggle Modal will dismiss the terminal modal if it is currently shown, so we must
-        // store the terminal back in the global
-        ProgramManager::insert_or_replace::<Terminal, _>(window, terminal_handle, cx);
-    } else {
-        // No connection was stored, create a new terminal
-        if let Some(closed_terminal_handle) = workspace.toggle_modal(cx, |workspace, cx| {
-            // No terminal modal visible, construct a new one.
-            let wd_strategy = cx
-                .global::<Settings>()
-                .terminal_overrides
-                .working_directory
-                .clone()
-                .unwrap_or(WorkingDirectory::CurrentProjectDirectory);
-
-            let working_directory = get_working_directory(workspace, cx, wd_strategy);
-
-            let this = cx.add_view(|cx| TerminalContainer::new(working_directory, true, cx));
-
-            if let TerminalContainerContent::Connected(connected) = &this.read(cx).content {
-                let terminal_handle = connected.read(cx).handle();
-                cx.subscribe(&terminal_handle, on_event).detach();
-                // Set the global immediately if terminal construction was successful,
-                // in case the user opens the command palette
-                ProgramManager::insert_or_replace::<Terminal, _>(window, terminal_handle, cx);
-            }
-
-            this
-        }) {
-            // Terminal modal was dismissed and the terminal view is connected, store the terminal
-            if let TerminalContainerContent::Connected(connected) =
-                &closed_terminal_handle.read(cx).content
-            {
-                let terminal_handle = connected.read(cx).handle();
-                // Set the global immediately if terminal construction was successful,
-                // in case the user opens the command palette
-                ProgramManager::insert_or_replace::<Terminal, _>(window, terminal_handle, cx);
-            }
-        }
-    }
-}
-
-pub fn on_event(
-    workspace: &mut Workspace,
-    _: ModelHandle<Terminal>,
-    event: &Event,
-    cx: &mut ViewContext<Workspace>,
-) {
-    // Dismiss the modal if the terminal quit
-    if let Event::CloseTerminal = event {
-        ProgramManager::remove::<Terminal, _>(cx.window_id(), cx);
-
-        if workspace.modal::<TerminalContainer>().is_some() {
-            workspace.dismiss_modal(cx)
-        }
-    }
-}

crates/terminal/src/terminal.rs 🔗

@@ -1,5 +1,4 @@
 pub mod mappings;
-pub mod modal;
 pub mod terminal_container_view;
 pub mod terminal_element;
 pub mod terminal_view;
@@ -32,7 +31,6 @@ use futures::{
 use mappings::mouse::{
     alt_scroll, grid_point, mouse_button_report, mouse_moved_report, mouse_side, scroll_report,
 };
-use modal::deploy_modal;
 
 use procinfo::LocalProcessInfo;
 use settings::{AlternateScroll, Settings, Shell, TerminalBlink};
@@ -55,9 +53,10 @@ use thiserror::Error;
 use gpui::{
     geometry::vector::{vec2f, Vector2F},
     keymap::Keystroke,
-    scene::{ClickRegionEvent, DownRegionEvent, DragRegionEvent, UpRegionEvent},
-    ClipboardItem, Entity, ModelContext, MouseButton, MouseMovedEvent, MutableAppContext,
-    ScrollWheelEvent, Task,
+    scene::{
+        ClickRegionEvent, DownRegionEvent, DragRegionEvent, ScrollWheelRegionEvent, UpRegionEvent,
+    },
+    ClipboardItem, Entity, ModelContext, MouseButton, MouseMovedEvent, MutableAppContext, Task,
 };
 
 use crate::mappings::{
@@ -68,8 +67,6 @@ use lazy_static::lazy_static;
 
 ///Initialize and register all of our action handlers
 pub fn init(cx: &mut MutableAppContext) {
-    cx.add_action(deploy_modal);
-
     terminal_view::init(cx);
     terminal_container_view::init(cx);
 }
@@ -1016,10 +1013,10 @@ impl Terminal {
     }
 
     ///Scroll the terminal
-    pub fn scroll_wheel(&mut self, e: &ScrollWheelEvent, origin: Vector2F) {
+    pub fn scroll_wheel(&mut self, e: ScrollWheelRegionEvent, origin: Vector2F) {
         let mouse_mode = self.mouse_mode(e.shift);
 
-        if let Some(scroll_lines) = self.determine_scroll_lines(e, mouse_mode) {
+        if let Some(scroll_lines) = self.determine_scroll_lines(&e, mouse_mode) {
             if mouse_mode {
                 let point = grid_point(
                     e.position.sub(origin),
@@ -1028,7 +1025,7 @@ impl Terminal {
                 );
 
                 if let Some(scrolls) =
-                    scroll_report(point, scroll_lines as i32, e, self.last_content.mode)
+                    scroll_report(point, scroll_lines as i32, &e, self.last_content.mode)
                 {
                     for scroll in scrolls {
                         self.pty_tx.notify(scroll);
@@ -1051,7 +1048,11 @@ impl Terminal {
         }
     }
 
-    fn determine_scroll_lines(&mut self, e: &ScrollWheelEvent, mouse_mode: bool) -> Option<i32> {
+    fn determine_scroll_lines(
+        &mut self,
+        e: &ScrollWheelRegionEvent,
+        mouse_mode: bool,
+    ) -> Option<i32> {
         let scroll_multiplier = if mouse_mode { 1. } else { SCROLL_MULTIPLIER };
 
         match e.phase {

crates/terminal/src/terminal_element.rs 🔗

@@ -373,7 +373,7 @@ impl TerminalElement {
     ) {
         let connection = self.terminal;
 
-        let mut region = MouseRegion::new(view_id, None, visible_bounds);
+        let mut region = MouseRegion::new::<Self>(view_id, view_id, visible_bounds);
 
         // Terminal Emulator controlled behavior:
         region = region
@@ -444,7 +444,14 @@ impl TerminalElement {
                         })
                     }
                 }
-            });
+            })
+            .on_scroll(TerminalElement::generic_button_handler(
+                connection,
+                origin,
+                move |terminal, origin, e, _cx| {
+                    terminal.scroll_wheel(e, origin);
+                },
+            ));
 
         // Mouse mode handlers:
         // All mouse modes need the extra click handlers
@@ -745,52 +752,40 @@ impl Element for TerminalElement {
     fn dispatch_event(
         &mut self,
         event: &gpui::Event,
-        bounds: gpui::geometry::rect::RectF,
-        visible_bounds: gpui::geometry::rect::RectF,
-        layout: &mut Self::LayoutState,
+        _bounds: gpui::geometry::rect::RectF,
+        _visible_bounds: gpui::geometry::rect::RectF,
+        _layout: &mut Self::LayoutState,
         _paint: &mut Self::PaintState,
         cx: &mut gpui::EventContext,
     ) -> bool {
-        match event {
-            Event::ScrollWheel(e) => visible_bounds
-                .contains_point(e.position)
-                .then(|| {
-                    let origin = bounds.origin() + vec2f(layout.size.cell_width, 0.);
-
-                    if let Some(terminal) = self.terminal.upgrade(cx.app) {
-                        terminal.update(cx.app, |term, _| term.scroll_wheel(e, origin));
-                        cx.notify();
-                    }
-                })
-                .is_some(),
-            Event::KeyDown(KeyDownEvent { keystroke, .. }) => {
-                if !cx.is_parent_view_focused() {
-                    return false;
-                }
+        if let Event::KeyDown(KeyDownEvent { keystroke, .. }) = event {
+            if !cx.is_parent_view_focused() {
+                return false;
+            }
 
-                if let Some(view) = self.view.upgrade(cx.app) {
-                    view.update(cx.app, |view, cx| {
-                        view.clear_bel(cx);
-                        view.pause_cursor_blinking(cx);
-                    })
-                }
+            if let Some(view) = self.view.upgrade(cx.app) {
+                view.update(cx.app, |view, cx| {
+                    view.clear_bel(cx);
+                    view.pause_cursor_blinking(cx);
+                })
+            }
 
-                self.terminal
-                    .upgrade(cx.app)
-                    .map(|model_handle| {
-                        model_handle.update(cx.app, |term, cx| {
-                            term.try_keystroke(
-                                keystroke,
-                                cx.global::<Settings>()
-                                    .terminal_overrides
-                                    .option_as_meta
-                                    .unwrap_or(false),
-                            )
-                        })
+            self.terminal
+                .upgrade(cx.app)
+                .map(|model_handle| {
+                    model_handle.update(cx.app, |term, cx| {
+                        term.try_keystroke(
+                            keystroke,
+                            cx.global::<Settings>()
+                                .terminal_overrides
+                                .option_as_meta
+                                .unwrap_or(false),
+                        )
                     })
-                    .unwrap_or(false)
-            }
-            _ => false,
+                })
+                .unwrap_or(false)
+        } else {
+            false
         }
     }
 

crates/terminal/src/terminal_view.rs 🔗

@@ -4,7 +4,7 @@ use alacritty_terminal::{index::Point, term::TermMode};
 use context_menu::{ContextMenu, ContextMenuItem};
 use gpui::{
     actions,
-    elements::{ChildView, ParentElement, Stack},
+    elements::{AnchorCorner, ChildView, ParentElement, Stack},
     geometry::vector::Vector2F,
     impl_internal_actions,
     keymap::Keystroke,
@@ -139,8 +139,9 @@ impl TerminalView {
             ContextMenuItem::item("Close Terminal", pane::CloseActiveItem),
         ];
 
-        self.context_menu
-            .update(cx, |menu, cx| menu.show(action.position, menu_entries, cx));
+        self.context_menu.update(cx, |menu, cx| {
+            menu.show(action.position, AnchorCorner::TopLeft, menu_entries, cx)
+        });
 
         cx.notify();
     }

crates/terminal/src/tests/terminal_test_context.rs 🔗

@@ -28,7 +28,9 @@ impl<'a> TerminalTestContext<'a> {
         let params = self.cx.update(AppState::test);
 
         let project = Project::test(params.fs.clone(), [], self.cx).await;
-        let (_, workspace) = self.cx.add_window(|cx| Workspace::new(project.clone(), cx));
+        let (_, workspace) = self
+            .cx
+            .add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
 
         (project, workspace)
     }

crates/theme/src/theme.rs 🔗

@@ -19,6 +19,7 @@ pub struct Theme {
     pub workspace: Workspace,
     pub context_menu: ContextMenu,
     pub chat_panel: ChatPanel,
+    pub contacts_popover: ContactsPopover,
     pub contacts_panel: ContactsPanel,
     pub contact_finder: ContactFinder,
     pub project_panel: ProjectPanel,
@@ -48,7 +49,7 @@ pub struct Workspace {
     pub pane_divider: Border,
     pub leader_border_opacity: f32,
     pub leader_border_width: f32,
-    pub sidebar_resize_handle: ContainerStyle,
+    pub sidebar: Sidebar,
     pub status_bar: StatusBar,
     pub toolbar: Toolbar,
     pub disconnected_overlay: ContainedText,
@@ -57,6 +58,7 @@ pub struct Workspace {
     pub notifications: Notifications,
     pub joining_project_avatar: ImageStyle,
     pub joining_project_message: ContainedText,
+    pub dock: Dock,
 }
 
 #[derive(Clone, Deserialize, Default)]
@@ -79,6 +81,7 @@ pub struct TabBar {
     #[serde(flatten)]
     pub container: ContainerStyle,
     pub pane_button: Interactive<IconButton>,
+    pub pane_button_container: ContainerStyle,
     pub active_pane: TabStyles,
     pub inactive_pane: TabStyles,
     pub dragged_tab: Tab,
@@ -149,6 +152,15 @@ pub struct Toolbar {
     pub nav_button: Interactive<IconButton>,
 }
 
+#[derive(Clone, Deserialize, Default)]
+pub struct Dock {
+    pub initial_size_right: f32,
+    pub initial_size_bottom: f32,
+    pub wash_color: Color,
+    pub panel: ContainerStyle,
+    pub maximized: ContainerStyle,
+}
+
 #[derive(Clone, Deserialize, Default)]
 pub struct Notifications {
     #[serde(flatten)]
@@ -231,7 +243,9 @@ pub struct StatusBarLspStatus {
 
 #[derive(Deserialize, Default)]
 pub struct Sidebar {
-    pub resize_handle: ContainerStyle,
+    pub initial_size: f32,
+    #[serde(flatten)]
+    pub container: ContainerStyle,
 }
 
 #[derive(Clone, Copy, Deserialize, Default)]
@@ -301,6 +315,11 @@ pub struct CommandPalette {
     pub keystroke_spacing: f32,
 }
 
+#[derive(Deserialize, Default)]
+pub struct ContactsPopover {
+    pub background: Color,
+}
+
 #[derive(Deserialize, Default)]
 pub struct ContactsPanel {
     #[serde(flatten)]
@@ -558,6 +577,7 @@ pub struct CodeActions {
 pub struct Interactive<T> {
     pub default: T,
     pub hover: Option<T>,
+    pub clicked: Option<T>,
     pub active: Option<T>,
     pub disabled: Option<T>,
 }
@@ -566,6 +586,8 @@ impl<T> Interactive<T> {
     pub fn style_for(&self, state: MouseState, active: bool) -> &T {
         if active {
             self.active.as_ref().unwrap_or(&self.default)
+        } else if state.clicked == Some(gpui::MouseButton::Left) && self.clicked.is_some() {
+            self.clicked.as_ref().unwrap()
         } else if state.hovered {
             self.hover.as_ref().unwrap_or(&self.default)
         } else {
@@ -588,6 +610,7 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
             #[serde(flatten)]
             default: Value,
             hover: Option<Value>,
+            clicked: Option<Value>,
             active: Option<Value>,
             disabled: Option<Value>,
         }
@@ -614,6 +637,7 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
         };
 
         let hover = deserialize_state(json.hover)?;
+        let clicked = deserialize_state(json.clicked)?;
         let active = deserialize_state(json.active)?;
         let disabled = deserialize_state(json.disabled)?;
         let default = serde_json::from_value(json.default).map_err(serde::de::Error::custom)?;
@@ -621,6 +645,7 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
         Ok(Interactive {
             default,
             hover,
+            clicked,
             active,
             disabled,
         })

crates/vim/src/vim_test_context.rs 🔗

@@ -39,7 +39,8 @@ impl<'a> VimTestContext<'a> {
             .insert_tree("/root", json!({ "dir": { "test.txt": "" } }))
             .await;
 
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
+        let (window_id, workspace) =
+            cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
 
         // Setup search toolbars
         workspace.update(cx, |workspace, cx| {

crates/workspace/src/dock.rs 🔗

@@ -0,0 +1,718 @@
+use collections::HashMap;
+use gpui::{
+    actions,
+    elements::{ChildView, Container, Empty, MouseEventHandler, Side, Svg},
+    impl_internal_actions, Border, CursorStyle, Element, ElementBox, Entity, MouseButton,
+    MutableAppContext, RenderContext, View, ViewContext, ViewHandle, WeakViewHandle,
+};
+use serde::Deserialize;
+use settings::{DockAnchor, Settings};
+use theme::Theme;
+
+use crate::{sidebar::SidebarSide, ItemHandle, Pane, StatusItemView, Workspace};
+
+#[derive(PartialEq, Clone, Deserialize)]
+pub struct MoveDock(pub DockAnchor);
+
+#[derive(PartialEq, Clone)]
+pub struct AddDefaultItemToDock;
+
+actions!(
+    dock,
+    [
+        FocusDock,
+        HideDock,
+        AnchorDockRight,
+        AnchorDockBottom,
+        ExpandDock
+    ]
+);
+impl_internal_actions!(dock, [MoveDock, AddDefaultItemToDock]);
+
+pub fn init(cx: &mut MutableAppContext) {
+    cx.add_action(Dock::focus_dock);
+    cx.add_action(Dock::hide_dock);
+    cx.add_action(Dock::move_dock);
+    cx.add_action(
+        |workspace: &mut Workspace, _: &AnchorDockRight, cx: &mut ViewContext<Workspace>| {
+            Dock::move_dock(workspace, &MoveDock(DockAnchor::Right), cx)
+        },
+    );
+    cx.add_action(
+        |workspace: &mut Workspace, _: &AnchorDockBottom, cx: &mut ViewContext<Workspace>| {
+            Dock::move_dock(workspace, &MoveDock(DockAnchor::Bottom), cx)
+        },
+    );
+    cx.add_action(
+        |workspace: &mut Workspace, _: &ExpandDock, cx: &mut ViewContext<Workspace>| {
+            Dock::move_dock(workspace, &MoveDock(DockAnchor::Expanded), cx)
+        },
+    );
+}
+
+#[derive(Copy, Clone, PartialEq, Eq, Debug)]
+pub enum DockPosition {
+    Shown(DockAnchor),
+    Hidden(DockAnchor),
+}
+
+impl Default for DockPosition {
+    fn default() -> Self {
+        DockPosition::Hidden(Default::default())
+    }
+}
+
+pub fn icon_for_dock_anchor(anchor: DockAnchor) -> &'static str {
+    match anchor {
+        DockAnchor::Right => "icons/dock_right_12.svg",
+        DockAnchor::Bottom => "icons/dock_bottom_12.svg",
+        DockAnchor::Expanded => "icons/dock_modal_12.svg",
+    }
+}
+
+impl DockPosition {
+    fn is_visible(&self) -> bool {
+        match self {
+            DockPosition::Shown(_) => true,
+            DockPosition::Hidden(_) => false,
+        }
+    }
+
+    fn anchor(&self) -> DockAnchor {
+        match self {
+            DockPosition::Shown(anchor) | DockPosition::Hidden(anchor) => *anchor,
+        }
+    }
+
+    fn hide(self) -> Self {
+        match self {
+            DockPosition::Shown(anchor) => DockPosition::Hidden(anchor),
+            DockPosition::Hidden(_) => self,
+        }
+    }
+
+    fn show(self) -> Self {
+        match self {
+            DockPosition::Hidden(anchor) => DockPosition::Shown(anchor),
+            DockPosition::Shown(_) => self,
+        }
+    }
+}
+
+pub type DefaultItemFactory =
+    fn(&mut Workspace, &mut ViewContext<Workspace>) -> Box<dyn ItemHandle>;
+
+pub struct Dock {
+    position: DockPosition,
+    panel_sizes: HashMap<DockAnchor, f32>,
+    pane: ViewHandle<Pane>,
+    default_item_factory: DefaultItemFactory,
+}
+
+impl Dock {
+    pub fn new(cx: &mut ViewContext<Workspace>, default_item_factory: DefaultItemFactory) -> Self {
+        let anchor = cx.global::<Settings>().default_dock_anchor;
+        let pane = cx.add_view(|cx| Pane::new(Some(anchor), cx));
+        pane.update(cx, |pane, cx| {
+            pane.set_active(false, cx);
+        });
+        let pane_id = pane.id();
+        cx.subscribe(&pane, move |workspace, _, event, cx| {
+            workspace.handle_pane_event(pane_id, event, cx);
+        })
+        .detach();
+
+        Self {
+            pane,
+            panel_sizes: Default::default(),
+            position: DockPosition::Hidden(anchor),
+            default_item_factory,
+        }
+    }
+
+    pub fn pane(&self) -> &ViewHandle<Pane> {
+        &self.pane
+    }
+
+    pub fn visible_pane(&self) -> Option<&ViewHandle<Pane>> {
+        self.position.is_visible().then(|| self.pane())
+    }
+
+    pub fn is_anchored_at(&self, anchor: DockAnchor) -> bool {
+        self.position.is_visible() && self.position.anchor() == anchor
+    }
+
+    fn set_dock_position(
+        workspace: &mut Workspace,
+        new_position: DockPosition,
+        cx: &mut ViewContext<Workspace>,
+    ) {
+        workspace.dock.position = new_position;
+        // Tell the pane about the new anchor position
+        workspace.dock.pane.update(cx, |pane, cx| {
+            pane.set_docked(Some(new_position.anchor()), cx)
+        });
+
+        if workspace.dock.position.is_visible() {
+            // Close the right sidebar if the dock is on the right side and the right sidebar is open
+            if workspace.dock.position.anchor() == DockAnchor::Right {
+                if workspace.right_sidebar().read(cx).is_open() {
+                    workspace.toggle_sidebar(SidebarSide::Right, cx);
+                }
+            }
+
+            // Ensure that the pane has at least one item or construct a default item to put in it
+            let pane = workspace.dock.pane.clone();
+            if pane.read(cx).items().next().is_none() {
+                let item_to_add = (workspace.dock.default_item_factory)(workspace, cx);
+                // Adding the item focuses the pane by default
+                Pane::add_item(workspace, &pane, item_to_add, true, true, None, cx);
+            } else {
+                cx.focus(pane);
+            }
+        } else if let Some(last_active_center_pane) = workspace.last_active_center_pane.clone() {
+            cx.focus(last_active_center_pane);
+        }
+        cx.emit(crate::Event::DockAnchorChanged);
+        cx.notify();
+    }
+
+    pub fn hide(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
+        Self::set_dock_position(workspace, workspace.dock.position.hide(), cx);
+    }
+
+    pub fn show(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
+        Self::set_dock_position(workspace, workspace.dock.position.show(), cx);
+    }
+
+    pub fn hide_on_sidebar_shown(
+        workspace: &mut Workspace,
+        sidebar_side: SidebarSide,
+        cx: &mut ViewContext<Workspace>,
+    ) {
+        if (sidebar_side == SidebarSide::Right && workspace.dock.is_anchored_at(DockAnchor::Right))
+            || workspace.dock.is_anchored_at(DockAnchor::Expanded)
+        {
+            Self::hide(workspace, cx);
+        }
+    }
+
+    fn focus_dock(workspace: &mut Workspace, _: &FocusDock, cx: &mut ViewContext<Workspace>) {
+        Self::set_dock_position(workspace, workspace.dock.position.show(), cx);
+    }
+
+    fn hide_dock(workspace: &mut Workspace, _: &HideDock, cx: &mut ViewContext<Workspace>) {
+        Self::set_dock_position(workspace, workspace.dock.position.hide(), cx);
+    }
+
+    fn move_dock(
+        workspace: &mut Workspace,
+        &MoveDock(new_anchor): &MoveDock,
+        cx: &mut ViewContext<Workspace>,
+    ) {
+        Self::set_dock_position(workspace, DockPosition::Shown(new_anchor), cx);
+    }
+
+    pub fn render(
+        &self,
+        theme: &Theme,
+        anchor: DockAnchor,
+        cx: &mut RenderContext<Workspace>,
+    ) -> Option<ElementBox> {
+        let style = &theme.workspace.dock;
+
+        self.position
+            .is_visible()
+            .then(|| self.position.anchor())
+            .filter(|current_anchor| *current_anchor == anchor)
+            .map(|anchor| match anchor {
+                DockAnchor::Bottom | DockAnchor::Right => {
+                    let mut panel_style = style.panel.clone();
+                    let (resize_side, initial_size) = if anchor == DockAnchor::Bottom {
+                        panel_style.border = Border {
+                            top: true,
+                            bottom: false,
+                            left: false,
+                            right: false,
+                            ..panel_style.border
+                        };
+
+                        (Side::Top, style.initial_size_bottom)
+                    } else {
+                        panel_style.border = Border {
+                            top: false,
+                            bottom: false,
+                            left: true,
+                            right: false,
+                            ..panel_style.border
+                        };
+                        (Side::Left, style.initial_size_right)
+                    };
+
+                    enum DockResizeHandle {}
+
+                    let resizable = Container::new(ChildView::new(self.pane.clone()).boxed())
+                        .with_style(panel_style)
+                        .with_resize_handle::<DockResizeHandle, _>(
+                            resize_side as usize,
+                            resize_side,
+                            4.,
+                            self.panel_sizes
+                                .get(&anchor)
+                                .copied()
+                                .unwrap_or(initial_size),
+                            cx,
+                        );
+
+                    let size = resizable.current_size();
+                    let workspace = cx.handle();
+                    cx.defer(move |cx| {
+                        if let Some(workspace) = workspace.upgrade(cx) {
+                            workspace.update(cx, |workspace, _| {
+                                workspace.dock.panel_sizes.insert(anchor, size);
+                            })
+                        }
+                    });
+
+                    resizable.flex(5., false).boxed()
+                }
+                DockAnchor::Expanded => {
+                    enum ExpandedDockWash {}
+                    enum ExpandedDockPane {}
+                    Container::new(
+                        MouseEventHandler::<ExpandedDockWash>::new(0, cx, |_state, cx| {
+                            MouseEventHandler::<ExpandedDockPane>::new(0, cx, |_state, _cx| {
+                                ChildView::new(self.pane.clone()).boxed()
+                            })
+                            .capture_all()
+                            .contained()
+                            .with_style(style.maximized)
+                            .boxed()
+                        })
+                        .capture_all()
+                        .on_down(MouseButton::Left, |_, cx| {
+                            cx.dispatch_action(HideDock);
+                        })
+                        .with_cursor_style(CursorStyle::Arrow)
+                        .boxed(),
+                    )
+                    .with_background_color(style.wash_color)
+                    .boxed()
+                }
+            })
+    }
+}
+
+pub struct ToggleDockButton {
+    workspace: WeakViewHandle<Workspace>,
+}
+
+impl ToggleDockButton {
+    pub fn new(workspace: ViewHandle<Workspace>, cx: &mut ViewContext<Self>) -> Self {
+        // When dock moves, redraw so that the icon and toggle status matches.
+        cx.subscribe(&workspace, |_, _, _, cx| cx.notify()).detach();
+
+        Self {
+            workspace: workspace.downgrade(),
+        }
+    }
+}
+
+impl Entity for ToggleDockButton {
+    type Event = ();
+}
+
+impl View for ToggleDockButton {
+    fn ui_name() -> &'static str {
+        "Dock Toggle"
+    }
+
+    fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
+        let workspace = self.workspace.upgrade(cx);
+
+        if workspace.is_none() {
+            return Empty::new().boxed();
+        }
+
+        let dock_position = workspace.unwrap().read(cx).dock.position;
+
+        let theme = cx.global::<Settings>().theme.clone();
+        let button = MouseEventHandler::<Self>::new(0, cx, {
+            let theme = theme.clone();
+            move |state, _| {
+                let style = theme
+                    .workspace
+                    .status_bar
+                    .sidebar_buttons
+                    .item
+                    .style_for(state, dock_position.is_visible());
+
+                Svg::new(icon_for_dock_anchor(dock_position.anchor()))
+                    .with_color(style.icon_color)
+                    .constrained()
+                    .with_width(style.icon_size)
+                    .with_height(style.icon_size)
+                    .contained()
+                    .with_style(style.container)
+                    .boxed()
+            }
+        })
+        .with_cursor_style(CursorStyle::PointingHand);
+
+        if dock_position.is_visible() {
+            button
+                .on_click(MouseButton::Left, |_, cx| {
+                    cx.dispatch_action(HideDock);
+                })
+                .with_tooltip::<Self, _>(
+                    0,
+                    "Hide Dock".into(),
+                    Some(Box::new(HideDock)),
+                    theme.tooltip.clone(),
+                    cx,
+                )
+        } else {
+            button
+                .on_click(MouseButton::Left, |_, cx| {
+                    cx.dispatch_action(FocusDock);
+                })
+                .with_tooltip::<Self, _>(
+                    0,
+                    "Focus Dock".into(),
+                    Some(Box::new(FocusDock)),
+                    theme.tooltip.clone(),
+                    cx,
+                )
+        }
+        .boxed()
+    }
+}
+
+impl StatusItemView for ToggleDockButton {
+    fn set_active_pane_item(
+        &mut self,
+        _active_pane_item: Option<&dyn crate::ItemHandle>,
+        _cx: &mut ViewContext<Self>,
+    ) {
+        //Not applicable
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use std::ops::{Deref, DerefMut};
+
+    use gpui::{AppContext, TestAppContext, UpdateView, ViewContext};
+    use project::{FakeFs, Project};
+    use settings::Settings;
+
+    use super::*;
+    use crate::{sidebar::Sidebar, tests::TestItem, ItemHandle, Workspace};
+
+    pub fn default_item_factory(
+        _workspace: &mut Workspace,
+        cx: &mut ViewContext<Workspace>,
+    ) -> Box<dyn ItemHandle> {
+        Box::new(cx.add_view(|_| TestItem::new()))
+    }
+
+    #[gpui::test]
+    async fn test_dock_hides_when_pane_empty(cx: &mut TestAppContext) {
+        let mut cx = DockTestContext::new(cx).await;
+
+        // Closing the last item in the dock hides the dock
+        cx.move_dock(DockAnchor::Right);
+        let old_items = cx.dock_items();
+        assert!(!old_items.is_empty());
+        cx.close_dock_items().await;
+        cx.assert_dock_position(DockPosition::Hidden(DockAnchor::Right));
+
+        // Reopening the dock adds a new item
+        cx.move_dock(DockAnchor::Right);
+        let new_items = cx.dock_items();
+        assert!(!new_items.is_empty());
+        assert!(new_items
+            .into_iter()
+            .all(|new_item| !old_items.contains(&new_item)));
+    }
+
+    #[gpui::test]
+    async fn test_dock_panel_collisions(cx: &mut TestAppContext) {
+        let mut cx = DockTestContext::new(cx).await;
+
+        // Dock closes when expanded for either panel
+        cx.move_dock(DockAnchor::Expanded);
+        cx.open_sidebar(SidebarSide::Left);
+        cx.assert_dock_position(DockPosition::Hidden(DockAnchor::Expanded));
+        cx.close_sidebar(SidebarSide::Left);
+        cx.move_dock(DockAnchor::Expanded);
+        cx.open_sidebar(SidebarSide::Right);
+        cx.assert_dock_position(DockPosition::Hidden(DockAnchor::Expanded));
+
+        // Dock closes in the right position if the right sidebar is opened
+        cx.move_dock(DockAnchor::Right);
+        cx.open_sidebar(SidebarSide::Left);
+        cx.assert_dock_position(DockPosition::Shown(DockAnchor::Right));
+        cx.open_sidebar(SidebarSide::Right);
+        cx.assert_dock_position(DockPosition::Hidden(DockAnchor::Right));
+        cx.close_sidebar(SidebarSide::Right);
+
+        // Dock in bottom position ignores sidebars
+        cx.move_dock(DockAnchor::Bottom);
+        cx.open_sidebar(SidebarSide::Left);
+        cx.open_sidebar(SidebarSide::Right);
+        cx.assert_dock_position(DockPosition::Shown(DockAnchor::Bottom));
+
+        // Opening the dock in the right position closes the right sidebar
+        cx.move_dock(DockAnchor::Right);
+        cx.assert_sidebar_closed(SidebarSide::Right);
+    }
+
+    #[gpui::test]
+    async fn test_focusing_panes_shows_and_hides_dock(cx: &mut TestAppContext) {
+        let mut cx = DockTestContext::new(cx).await;
+
+        // Focusing an item not in the dock when expanded hides the dock
+        let center_item = cx.add_item_to_center_pane();
+        cx.move_dock(DockAnchor::Expanded);
+        let dock_item = cx
+            .dock_items()
+            .get(0)
+            .cloned()
+            .expect("Dock should have an item at this point");
+        center_item.update(&mut cx, |_, cx| cx.focus_self());
+        cx.assert_dock_position(DockPosition::Hidden(DockAnchor::Expanded));
+
+        // Focusing an item not in the dock when not expanded, leaves the dock open but inactive
+        cx.move_dock(DockAnchor::Right);
+        center_item.update(&mut cx, |_, cx| cx.focus_self());
+        cx.assert_dock_position(DockPosition::Shown(DockAnchor::Right));
+        cx.assert_dock_pane_inactive();
+        cx.assert_workspace_pane_active();
+
+        // Focusing an item in the dock activates it's pane
+        dock_item.update(&mut cx, |_, cx| cx.focus_self());
+        cx.assert_dock_position(DockPosition::Shown(DockAnchor::Right));
+        cx.assert_dock_pane_active();
+        cx.assert_workspace_pane_inactive();
+    }
+
+    #[gpui::test]
+    async fn test_toggle_dock_focus(cx: &mut TestAppContext) {
+        let cx = DockTestContext::new(cx).await;
+
+        cx.move_dock(DockAnchor::Right);
+        cx.assert_dock_pane_active();
+        cx.hide_dock();
+        cx.move_dock(DockAnchor::Right);
+        cx.assert_dock_pane_active();
+    }
+
+    struct DockTestContext<'a> {
+        pub cx: &'a mut TestAppContext,
+        pub window_id: usize,
+        pub workspace: ViewHandle<Workspace>,
+    }
+
+    impl<'a> DockTestContext<'a> {
+        pub async fn new(cx: &'a mut TestAppContext) -> DockTestContext<'a> {
+            Settings::test_async(cx);
+            let fs = FakeFs::new(cx.background());
+
+            cx.update(|cx| init(cx));
+            let project = Project::test(fs, [], cx).await;
+            let (window_id, workspace) =
+                cx.add_window(|cx| Workspace::new(project, default_item_factory, cx));
+
+            workspace.update(cx, |workspace, cx| {
+                let left_panel = cx.add_view(|_| TestItem::new());
+                workspace.left_sidebar().update(cx, |sidebar, cx| {
+                    sidebar.add_item(
+                        "icons/folder_tree_16.svg",
+                        "Left Test Panel".to_string(),
+                        left_panel.clone(),
+                        cx,
+                    );
+                });
+
+                let right_panel = cx.add_view(|_| TestItem::new());
+                workspace.right_sidebar().update(cx, |sidebar, cx| {
+                    sidebar.add_item(
+                        "icons/folder_tree_16.svg",
+                        "Right Test Panel".to_string(),
+                        right_panel.clone(),
+                        cx,
+                    );
+                });
+            });
+
+            Self {
+                cx,
+                window_id,
+                workspace,
+            }
+        }
+
+        pub fn workspace<F, T>(&self, read: F) -> T
+        where
+            F: FnOnce(&Workspace, &AppContext) -> T,
+        {
+            self.workspace.read_with(self.cx, read)
+        }
+
+        pub fn update_workspace<F, T>(&mut self, update: F) -> T
+        where
+            F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
+        {
+            self.workspace.update(self.cx, update)
+        }
+
+        pub fn sidebar<F, T>(&self, sidebar_side: SidebarSide, read: F) -> T
+        where
+            F: FnOnce(&Sidebar, &AppContext) -> T,
+        {
+            self.workspace(|workspace, cx| {
+                let sidebar = match sidebar_side {
+                    SidebarSide::Left => workspace.left_sidebar(),
+                    SidebarSide::Right => workspace.right_sidebar(),
+                }
+                .read(cx);
+
+                read(sidebar, cx)
+            })
+        }
+
+        pub fn center_pane_handle(&self) -> ViewHandle<Pane> {
+            self.workspace(|workspace, _| {
+                workspace
+                    .last_active_center_pane
+                    .clone()
+                    .unwrap_or_else(|| workspace.center.panes()[0].clone())
+            })
+        }
+
+        pub fn add_item_to_center_pane(&mut self) -> ViewHandle<TestItem> {
+            self.update_workspace(|workspace, cx| {
+                let item = cx.add_view(|_| TestItem::new());
+                let pane = workspace
+                    .last_active_center_pane
+                    .clone()
+                    .unwrap_or_else(|| workspace.center.panes()[0].clone());
+                Pane::add_item(
+                    workspace,
+                    &pane,
+                    Box::new(item.clone()),
+                    true,
+                    true,
+                    None,
+                    cx,
+                );
+                item
+            })
+        }
+
+        pub fn dock_pane<F, T>(&self, read: F) -> T
+        where
+            F: FnOnce(&Pane, &AppContext) -> T,
+        {
+            self.workspace(|workspace, cx| {
+                let dock_pane = workspace.dock_pane().read(cx);
+                read(dock_pane, cx)
+            })
+        }
+
+        pub fn move_dock(&self, anchor: DockAnchor) {
+            self.cx.dispatch_action(self.window_id, MoveDock(anchor));
+        }
+
+        pub fn hide_dock(&self) {
+            self.cx.dispatch_action(self.window_id, HideDock);
+        }
+
+        pub fn open_sidebar(&mut self, sidebar_side: SidebarSide) {
+            if !self.sidebar(sidebar_side, |sidebar, _| sidebar.is_open()) {
+                self.update_workspace(|workspace, cx| workspace.toggle_sidebar(sidebar_side, cx));
+            }
+        }
+
+        pub fn close_sidebar(&mut self, sidebar_side: SidebarSide) {
+            if self.sidebar(sidebar_side, |sidebar, _| sidebar.is_open()) {
+                self.update_workspace(|workspace, cx| workspace.toggle_sidebar(sidebar_side, cx));
+            }
+        }
+
+        pub fn dock_items(&self) -> Vec<ViewHandle<TestItem>> {
+            self.dock_pane(|pane, cx| {
+                pane.items()
+                    .map(|item| {
+                        item.act_as::<TestItem>(cx)
+                            .expect("Dock Test Context uses TestItems in the dock")
+                    })
+                    .collect()
+            })
+        }
+
+        pub async fn close_dock_items(&mut self) {
+            self.update_workspace(|workspace, cx| {
+                Pane::close_items(workspace, workspace.dock_pane().clone(), cx, |_| true)
+            })
+            .await
+            .expect("Could not close dock items")
+        }
+
+        pub fn assert_dock_position(&self, expected_position: DockPosition) {
+            self.workspace(|workspace, _| assert_eq!(workspace.dock.position, expected_position));
+        }
+
+        pub fn assert_sidebar_closed(&self, sidebar_side: SidebarSide) {
+            assert!(!self.sidebar(sidebar_side, |sidebar, _| sidebar.is_open()));
+        }
+
+        pub fn assert_workspace_pane_active(&self) {
+            assert!(self
+                .center_pane_handle()
+                .read_with(self.cx, |pane, _| pane.is_active()));
+        }
+
+        pub fn assert_workspace_pane_inactive(&self) {
+            assert!(!self
+                .center_pane_handle()
+                .read_with(self.cx, |pane, _| pane.is_active()));
+        }
+
+        pub fn assert_dock_pane_active(&self) {
+            assert!(self.dock_pane(|pane, _| pane.is_active()))
+        }
+
+        pub fn assert_dock_pane_inactive(&self) {
+            assert!(!self.dock_pane(|pane, _| pane.is_active()))
+        }
+    }
+
+    impl<'a> Deref for DockTestContext<'a> {
+        type Target = gpui::TestAppContext;
+
+        fn deref(&self) -> &Self::Target {
+            self.cx
+        }
+    }
+
+    impl<'a> DerefMut for DockTestContext<'a> {
+        fn deref_mut(&mut self) -> &mut Self::Target {
+            &mut self.cx
+        }
+    }
+
+    impl<'a> UpdateView for DockTestContext<'a> {
+        fn update_view<T, S>(
+            &mut self,
+            handle: &ViewHandle<T>,
+            update: &mut dyn FnMut(&mut T, &mut ViewContext<T>) -> S,
+        ) -> S
+        where
+            T: View,
+        {
+            handle.update(self.cx, update)
+        }
+    }
+}

crates/workspace/src/pane.rs 🔗

@@ -1,5 +1,9 @@
 use super::{ItemHandle, SplitDirection};
-use crate::{toolbar::Toolbar, Item, NewFile, NewSearch, NewTerminal, WeakItemHandle, Workspace};
+use crate::{
+    dock::{icon_for_dock_anchor, AnchorDockBottom, AnchorDockRight, ExpandDock, HideDock},
+    toolbar::Toolbar,
+    Item, NewFile, NewSearch, NewTerminal, WeakItemHandle, Workspace,
+};
 use anyhow::Result;
 use collections::{HashMap, HashSet, VecDeque};
 use context_menu::{ContextMenu, ContextMenuItem};
@@ -15,13 +19,13 @@ use gpui::{
     },
     impl_actions, impl_internal_actions,
     platform::{CursorStyle, NavigationDirection},
-    AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, EventContext,
+    Action, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, EventContext,
     ModelHandle, MouseButton, MutableAppContext, PromptLevel, Quad, RenderContext, Task, View,
     ViewContext, ViewHandle, WeakViewHandle,
 };
 use project::{Project, ProjectEntryId, ProjectPath};
 use serde::Deserialize;
-use settings::{Autosave, Settings};
+use settings::{Autosave, DockAnchor, Settings};
 use std::{any::Any, cell::RefCell, cmp, mem, path::Path, rc::Rc};
 use theme::Theme;
 use util::ResultExt;
@@ -76,13 +80,27 @@ pub struct DeploySplitMenu {
     position: Vector2F,
 }
 
+#[derive(Clone, PartialEq)]
+pub struct DeployDockMenu {
+    position: Vector2F,
+}
+
 #[derive(Clone, PartialEq)]
 pub struct DeployNewMenu {
     position: Vector2F,
 }
 
 impl_actions!(pane, [GoBack, GoForward, ActivateItem]);
-impl_internal_actions!(pane, [CloseItem, DeploySplitMenu, DeployNewMenu, MoveItem]);
+impl_internal_actions!(
+    pane,
+    [
+        CloseItem,
+        DeploySplitMenu,
+        DeployNewMenu,
+        DeployDockMenu,
+        MoveItem
+    ]
+);
 
 const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
 
@@ -141,6 +159,7 @@ pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(|pane: &mut Pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx));
     cx.add_action(Pane::deploy_split_menu);
     cx.add_action(Pane::deploy_new_menu);
+    cx.add_action(Pane::deploy_dock_menu);
     cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
         Pane::reopen_closed_item(workspace, cx).detach();
     });
@@ -168,6 +187,7 @@ pub fn init(cx: &mut MutableAppContext) {
     });
 }
 
+#[derive(Debug)]
 pub enum Event {
     Focused,
     ActivateItem { local: bool },
@@ -185,7 +205,8 @@ pub struct Pane {
     autoscroll: bool,
     nav_history: Rc<RefCell<NavHistory>>,
     toolbar: ViewHandle<Toolbar>,
-    context_menu: ViewHandle<ContextMenu>,
+    tab_bar_context_menu: ViewHandle<ContextMenu>,
+    docked: Option<DockAnchor>,
 }
 
 pub struct ItemNavHistory {
@@ -235,7 +256,7 @@ pub enum ReorderBehavior {
 }
 
 impl Pane {
-    pub fn new(cx: &mut ViewContext<Self>) -> Self {
+    pub fn new(docked: Option<DockAnchor>, cx: &mut ViewContext<Self>) -> Self {
         let handle = cx.weak_handle();
         let context_menu = cx.add_view(ContextMenu::new);
         Self {
@@ -253,15 +274,25 @@ impl Pane {
                 pane: handle.clone(),
             })),
             toolbar: cx.add_view(|_| Toolbar::new(handle)),
-            context_menu,
+            tab_bar_context_menu: context_menu,
+            docked,
         }
     }
 
+    pub fn is_active(&self) -> bool {
+        self.is_active
+    }
+
     pub fn set_active(&mut self, is_active: bool, cx: &mut ViewContext<Self>) {
         self.is_active = is_active;
         cx.notify();
     }
 
+    pub fn set_docked(&mut self, docked: Option<DockAnchor>, cx: &mut ViewContext<Self>) {
+        self.docked = docked;
+        cx.notify();
+    }
+
     pub fn nav_history_for_item<T: Item>(&self, item: &ViewHandle<T>) -> ItemNavHistory {
         ItemNavHistory {
             history: self.nav_history.clone(),
@@ -675,7 +706,7 @@ impl Pane {
         pane: ViewHandle<Pane>,
         item_id_to_close: usize,
         cx: &mut ViewContext<Workspace>,
-    ) -> Task<Result<bool>> {
+    ) -> Task<Result<()>> {
         Self::close_items(workspace, pane, cx, move |view_id| {
             view_id == item_id_to_close
         })
@@ -686,7 +717,7 @@ impl Pane {
         pane: ViewHandle<Pane>,
         cx: &mut ViewContext<Workspace>,
         should_close: impl 'static + Fn(usize) -> bool,
-    ) -> Task<Result<bool>> {
+    ) -> Task<Result<()>> {
         let project = workspace.project().clone();
 
         // Find the items to close.
@@ -759,7 +790,7 @@ impl Pane {
             }
 
             pane.update(&mut cx, |_, cx| cx.notify());
-            Ok(true)
+            Ok(())
         })
     }
 
@@ -962,9 +993,10 @@ impl Pane {
     }
 
     fn deploy_split_menu(&mut self, action: &DeploySplitMenu, cx: &mut ViewContext<Self>) {
-        self.context_menu.update(cx, |menu, cx| {
+        self.tab_bar_context_menu.update(cx, |menu, cx| {
             menu.show(
                 action.position,
+                AnchorCorner::TopRight,
                 vec![
                     ContextMenuItem::item("Split Right", SplitRight),
                     ContextMenuItem::item("Split Left", SplitLeft),
@@ -976,10 +1008,26 @@ impl Pane {
         });
     }
 
+    fn deploy_dock_menu(&mut self, action: &DeployDockMenu, cx: &mut ViewContext<Self>) {
+        self.tab_bar_context_menu.update(cx, |menu, cx| {
+            menu.show(
+                action.position,
+                AnchorCorner::TopRight,
+                vec![
+                    ContextMenuItem::item("Anchor Dock Right", AnchorDockRight),
+                    ContextMenuItem::item("Anchor Dock Bottom", AnchorDockBottom),
+                    ContextMenuItem::item("Expand Dock", ExpandDock),
+                ],
+                cx,
+            );
+        });
+    }
+
     fn deploy_new_menu(&mut self, action: &DeployNewMenu, cx: &mut ViewContext<Self>) {
-        self.context_menu.update(cx, |menu, cx| {
+        self.tab_bar_context_menu.update(cx, |menu, cx| {
             menu.show(
                 action.position,
+                AnchorCorner::TopRight,
                 vec![
                     ContextMenuItem::item("New File", NewFile),
                     ContextMenuItem::item("New Terminal", NewTerminal),
@@ -1004,7 +1052,7 @@ impl Pane {
         });
     }
 
-    fn render_tab_bar(&mut self, cx: &mut RenderContext<Self>) -> impl Element {
+    fn render_tabs(&mut self, cx: &mut RenderContext<Self>) -> impl Element {
         let theme = cx.global::<Settings>().theme.clone();
         let filler_index = self.items.len();
 
@@ -1012,7 +1060,7 @@ impl Pane {
         enum Tab {}
         enum Filler {}
         let pane = cx.handle();
-        MouseEventHandler::new::<Tabs, _, _>(0, cx, |_, cx| {
+        MouseEventHandler::<Tabs>::new(0, cx, |_, cx| {
             let autoscroll = if mem::take(&mut self.autoscroll) {
                 Some(self.active_item_index)
             } else {
@@ -1033,7 +1081,7 @@ impl Pane {
                 let tab_active = ix == self.active_item_index;
 
                 row.add_child({
-                    MouseEventHandler::new::<Tab, _, _>(ix, cx, {
+                    MouseEventHandler::<Tab>::new(ix, cx, {
                         let item = item.clone();
                         let pane = pane.clone();
                         let detail = detail.clone();
@@ -1047,6 +1095,7 @@ impl Pane {
                             Self::render_tab(
                                 &item,
                                 pane,
+                                ix == 0,
                                 detail,
                                 hovered,
                                 Self::tab_overlay_color(hovered, theme.as_ref(), cx),
@@ -1091,6 +1140,7 @@ impl Pane {
                                 Self::render_tab(
                                     &dragged_item.item,
                                     dragged_item.pane.clone(),
+                                    false,
                                     detail,
                                     false,
                                     None,
@@ -1108,7 +1158,7 @@ impl Pane {
             // the filler
             let filler_style = theme.workspace.tab_bar.tab_style(pane_active, false);
             row.add_child(
-                MouseEventHandler::new::<Filler, _, _>(0, cx, |mouse_state, cx| {
+                MouseEventHandler::<Filler>::new(0, cx, |mouse_state, cx| {
                     let mut filler = Empty::new()
                         .contained()
                         .with_style(filler_style.container)
@@ -1172,6 +1222,7 @@ impl Pane {
     fn render_tab<V: View>(
         item: &Box<dyn ItemHandle>,
         pane: WeakViewHandle<Pane>,
+        first: bool,
         detail: Option<usize>,
         hovered: bool,
         overlay: Option<Color>,
@@ -1179,6 +1230,10 @@ impl Pane {
         cx: &mut RenderContext<V>,
     ) -> ElementBox {
         let title = item.tab_content(detail, &tab_style, cx);
+        let mut container = tab_style.container.clone();
+        if first {
+            container.border.left = false;
+        }
 
         let mut tab = Flex::row()
             .with_child(
@@ -1230,17 +1285,13 @@ impl Pane {
                         let item_id = item.id();
                         enum TabCloseButton {}
                         let icon = Svg::new("icons/x_mark_thin_8.svg");
-                        MouseEventHandler::new::<TabCloseButton, _, _>(
-                            item_id,
-                            cx,
-                            |mouse_state, _| {
-                                if mouse_state.hovered {
-                                    icon.with_color(tab_style.icon_close_active).boxed()
-                                } else {
-                                    icon.with_color(tab_style.icon_close).boxed()
-                                }
-                            },
-                        )
+                        MouseEventHandler::<TabCloseButton>::new(item_id, cx, |mouse_state, _| {
+                            if mouse_state.hovered {
+                                icon.with_color(tab_style.icon_close_active).boxed()
+                            } else {
+                                icon.with_color(tab_style.icon_close).boxed()
+                            }
+                        })
                         .with_padding(Padding::uniform(4.))
                         .with_cursor_style(CursorStyle::PointingHand)
                         .on_click(MouseButton::Left, {
@@ -1263,7 +1314,7 @@ impl Pane {
                 .boxed(),
             )
             .contained()
-            .with_style(tab_style.container);
+            .with_style(container);
 
         if let Some(overlay) = overlay {
             tab = tab.with_overlay_color(overlay);
@@ -1316,120 +1367,123 @@ impl View for Pane {
     }
 
     fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
-        enum SplitIcon {}
-
         let this = cx.handle();
 
+        enum MouseNavigationHandler {}
+
         Stack::new()
             .with_child(
-                EventHandler::new(if let Some(active_item) = self.active_item() {
-                    Flex::column()
-                        .with_child({
-                            let mut tab_row = Flex::row()
-                                .with_child(self.render_tab_bar(cx).flex(1., true).named("tabs"));
-
-                            if self.is_active {
-                                tab_row.add_children([
-                                    MouseEventHandler::new::<SplitIcon, _, _>(
-                                        0,
-                                        cx,
-                                        |mouse_state, cx| {
-                                            let theme =
-                                                &cx.global::<Settings>().theme.workspace.tab_bar;
-                                            let style =
-                                                theme.pane_button.style_for(mouse_state, false);
-                                            Svg::new("icons/plus_12.svg")
-                                                .with_color(style.color)
-                                                .constrained()
-                                                .with_width(style.icon_width)
-                                                .aligned()
-                                                .contained()
-                                                .with_style(style.container)
-                                                .constrained()
-                                                .with_width(style.button_width)
-                                                .with_height(style.button_width)
-                                                .aligned()
-                                                .boxed()
-                                        },
+                MouseEventHandler::<MouseNavigationHandler>::new(0, cx, |_, cx| {
+                    if let Some(active_item) = self.active_item() {
+                        Flex::column()
+                            .with_child({
+                                let mut tab_row = Flex::row()
+                                    .with_child(self.render_tabs(cx).flex(1.0, true).named("tabs"));
+
+                                // Render pane buttons
+                                let theme = cx.global::<Settings>().theme.clone();
+                                if self.is_active {
+                                    tab_row.add_child(
+                                        Flex::row()
+                                            // New menu
+                                            .with_child(tab_bar_button(
+                                                0,
+                                                "icons/plus_12.svg",
+                                                cx,
+                                                |position| DeployNewMenu { position },
+                                            ))
+                                            .with_child(
+                                                self.docked
+                                                    .map(|anchor| {
+                                                        // Add the dock menu button if this pane is a dock
+                                                        let dock_icon =
+                                                            icon_for_dock_anchor(anchor);
+
+                                                        tab_bar_button(
+                                                            1,
+                                                            dock_icon,
+                                                            cx,
+                                                            |position| DeployDockMenu { position },
+                                                        )
+                                                    })
+                                                    .unwrap_or_else(|| {
+                                                        // Add the split menu if this pane is not a dock
+                                                        tab_bar_button(
+                                                            2,
+                                                            "icons/split_12.svg",
+                                                            cx,
+                                                            |position| DeploySplitMenu { position },
+                                                        )
+                                                    }),
+                                            )
+                                            // Add the close dock button if this pane is a dock
+                                            .with_children(self.docked.map(|_| {
+                                                tab_bar_button(
+                                                    3,
+                                                    "icons/x_mark_thin_8.svg",
+                                                    cx,
+                                                    |_| HideDock,
+                                                )
+                                            }))
+                                            .contained()
+                                            .with_style(
+                                                theme.workspace.tab_bar.pane_button_container,
+                                            )
+                                            .flex(1., false)
+                                            .boxed(),
                                     )
-                                    .with_cursor_style(CursorStyle::PointingHand)
-                                    .on_down(MouseButton::Left, |e, cx| {
-                                        cx.dispatch_action(DeployNewMenu {
-                                            position: e.position,
-                                        });
-                                    })
-                                    .boxed(),
-                                    MouseEventHandler::new::<SplitIcon, _, _>(
-                                        1,
-                                        cx,
-                                        |mouse_state, cx| {
-                                            let theme =
-                                                &cx.global::<Settings>().theme.workspace.tab_bar;
-                                            let style =
-                                                theme.pane_button.style_for(mouse_state, false);
-                                            Svg::new("icons/split_12.svg")
-                                                .with_color(style.color)
-                                                .constrained()
-                                                .with_width(style.icon_width)
-                                                .aligned()
-                                                .contained()
-                                                .with_style(style.container)
-                                                .constrained()
-                                                .with_width(style.button_width)
-                                                .with_height(style.button_width)
-                                                .aligned()
-                                                .boxed()
-                                        },
-                                    )
-                                    .with_cursor_style(CursorStyle::PointingHand)
-                                    .on_down(MouseButton::Left, |e, cx| {
-                                        cx.dispatch_action(DeploySplitMenu {
-                                            position: e.position,
-                                        });
-                                    })
-                                    .boxed(),
-                                ])
-                            }
+                                }
 
-                            tab_row
-                                .constrained()
-                                .with_height(cx.global::<Settings>().theme.workspace.tab_bar.height)
-                                .named("tab bar")
+                                tab_row
+                                    .constrained()
+                                    .with_height(theme.workspace.tab_bar.height)
+                                    .contained()
+                                    .with_style(theme.workspace.tab_bar.container)
+                                    .flex(1., false)
+                                    .named("tab bar")
+                            })
+                            .with_child(ChildView::new(&self.toolbar).expanded().boxed())
+                            .with_child(ChildView::new(active_item).flex(1., true).boxed())
+                            .boxed()
+                    } else {
+                        enum EmptyPane {}
+                        let theme = cx.global::<Settings>().theme.clone();
+
+                        MouseEventHandler::<EmptyPane>::new(0, cx, |_, _| {
+                            Empty::new()
+                                .contained()
+                                .with_background_color(theme.workspace.background)
+                                .boxed()
+                        })
+                        .on_down(MouseButton::Left, |_, cx| {
+                            cx.focus_parent_view();
+                        })
+                        .on_up(MouseButton::Left, {
+                            let pane = this.clone();
+                            move |_, cx: &mut EventContext| Pane::handle_dropped_item(&pane, 0, cx)
                         })
-                        .with_child(ChildView::new(&self.toolbar).boxed())
-                        .with_child(ChildView::new(active_item).flex(1., true).boxed())
                         .boxed()
-                } else {
-                    enum EmptyPane {}
-                    let theme = cx.global::<Settings>().theme.clone();
-
-                    MouseEventHandler::new::<EmptyPane, _, _>(0, cx, |_, _| {
-                        Empty::new()
-                            .contained()
-                            .with_background_color(theme.workspace.background)
-                            .boxed()
-                    })
-                    .on_down(MouseButton::Left, |_, cx| {
-                        cx.focus_parent_view();
-                    })
-                    .boxed()
+                    }
                 })
-                .on_navigate_mouse_down(move |direction, cx| {
+                .on_down(MouseButton::Navigate(NavigationDirection::Back), {
                     let this = this.clone();
-                    match direction {
-                        NavigationDirection::Back => {
-                            cx.dispatch_action(GoBack { pane: Some(this) })
-                        }
-                        NavigationDirection::Forward => {
-                            cx.dispatch_action(GoForward { pane: Some(this) })
-                        }
+                    move |_, cx| {
+                        cx.dispatch_action(GoBack {
+                            pane: Some(this.clone()),
+                        });
+                    }
+                })
+                .on_down(MouseButton::Navigate(NavigationDirection::Forward), {
+                    let this = this.clone();
+                    move |_, cx| {
+                        cx.dispatch_action(GoForward {
+                            pane: Some(this.clone()),
+                        })
                     }
-
-                    true
                 })
                 .boxed(),
             )
-            .with_child(ChildView::new(&self.context_menu).boxed())
+            .with_child(ChildView::new(&self.tab_bar_context_menu).boxed())
             .named("pane")
     }
 
@@ -1451,6 +1505,36 @@ impl View for Pane {
     }
 }
 
+fn tab_bar_button<A: Action>(
+    index: usize,
+    icon: &'static str,
+    cx: &mut RenderContext<Pane>,
+    action_builder: impl 'static + Fn(Vector2F) -> A,
+) -> ElementBox {
+    enum TabBarButton {}
+
+    MouseEventHandler::<TabBarButton>::new(index, cx, |mouse_state, cx| {
+        let theme = &cx.global::<Settings>().theme.workspace.tab_bar;
+        let style = theme.pane_button.style_for(mouse_state, false);
+        Svg::new(icon)
+            .with_color(style.color)
+            .constrained()
+            .with_width(style.icon_width)
+            .aligned()
+            .constrained()
+            .with_width(style.button_width)
+            .with_height(style.button_width)
+            // .aligned()
+            .boxed()
+    })
+    .with_cursor_style(CursorStyle::PointingHand)
+    .on_click(MouseButton::Left, move |e, cx| {
+        cx.dispatch_action(action_builder(e.region.lower_right()));
+    })
+    .flex(1., false)
+    .boxed()
+}
+
 impl ItemNavHistory {
     pub fn push<D: 'static + Any>(&self, data: Option<D>, cx: &mut MutableAppContext) {
         self.history.borrow_mut().push(data, self.item.clone(), cx);
@@ -1566,7 +1650,8 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+        let (_, workspace) =
+            cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
         // 1. Add with a destination index
@@ -1654,7 +1739,8 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+        let (_, workspace) =
+            cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
         // 1. Add with a destination index
@@ -1730,7 +1816,8 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+        let (_, workspace) =
+            cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
         // singleton view

crates/workspace/src/pane_group.rs 🔗

@@ -38,6 +38,10 @@ impl PaneGroup {
         }
     }
 
+    /// Returns:
+    /// - Ok(true) if it found and removed a pane
+    /// - Ok(false) if it found but did not remove the pane
+    /// - Err(_) if it did not find the pane
     pub fn remove(&mut self, pane: &ViewHandle<Pane>) -> Result<bool> {
         match &mut self.root {
             Member::Pane(_) => Ok(false),

crates/workspace/src/programs.rs 🔗

@@ -1,77 +0,0 @@
-// TODO: Need to put this basic structure in workspace, and make 'program handles'
-// based off of the 'searchable item' pattern except with models. This way, the workspace's clients
-// can register their models as programs with a specific identity and capable of notifying the workspace
-// Programs are:
-//  - Kept alive by the program manager, they need to emit an event to get dropped from it
-//  - Can be interacted with directly, (closed, activated, etc.) by the program manager, bypassing
-//    associated view(s)
-//  - Have special rendering methods that the program manager requires them to implement to fill out
-//    the status bar
-//  - Can emit events for the program manager which:
-//    - Add a jewel (notification, change, etc.)
-//    - Drop the program
-//    - ???
-//  - Program Manager is kept in a global, listens for window drop so it can drop all it's program handles
-
-use collections::HashMap;
-use gpui::{AnyModelHandle, Entity, ModelHandle, View, ViewContext};
-
-/// This struct is going to be the starting point for the 'program manager' feature that will
-/// eventually be implemented to provide a collaborative way of engaging with identity-having
-/// features like the terminal.
-pub struct ProgramManager {
-    // TODO: Make this a hashset or something
-    modals: HashMap<usize, AnyModelHandle>,
-}
-
-impl ProgramManager {
-    pub fn insert_or_replace<T: Entity, V: View>(
-        window: usize,
-        program: ModelHandle<T>,
-        cx: &mut ViewContext<V>,
-    ) -> Option<AnyModelHandle> {
-        cx.update_global::<ProgramManager, _, _>(|pm, _| {
-            pm.insert_or_replace_internal::<T>(window, program)
-        })
-    }
-
-    pub fn remove<T: Entity, V: View>(
-        window: usize,
-        cx: &mut ViewContext<V>,
-    ) -> Option<ModelHandle<T>> {
-        cx.update_global::<ProgramManager, _, _>(|pm, _| pm.remove_internal::<T>(window))
-    }
-
-    pub fn new() -> Self {
-        Self {
-            modals: Default::default(),
-        }
-    }
-
-    /// Inserts or replaces the model at the given location.
-    fn insert_or_replace_internal<T: Entity>(
-        &mut self,
-        window: usize,
-        program: ModelHandle<T>,
-    ) -> Option<AnyModelHandle> {
-        self.modals.insert(window, AnyModelHandle::from(program))
-    }
-
-    /// Remove the program associated with this window, if it's of the given type
-    fn remove_internal<T: Entity>(&mut self, window: usize) -> Option<ModelHandle<T>> {
-        let program = self.modals.remove(&window);
-        if let Some(program) = program {
-            if program.is::<T>() {
-                // Guaranteed to be some, but leave it in the option
-                // anyway for the API
-                program.downcast()
-            } else {
-                // Model is of the incorrect type, put it back
-                self.modals.insert(window, program);
-                None
-            }
-        } else {
-            None
-        }
-    }
-}

crates/workspace/src/sidebar.rs 🔗

@@ -5,14 +5,15 @@ use gpui::{
 };
 use serde::Deserialize;
 use settings::Settings;
-use std::{cell::RefCell, rc::Rc};
-use theme::Theme;
+use std::rc::Rc;
 
 pub trait SidebarItem: View {
     fn should_activate_item_on_event(&self, _: &Self::Event, _: &AppContext) -> bool {
         false
     }
-    fn should_show_badge(&self, cx: &AppContext) -> bool;
+    fn should_show_badge(&self, _: &AppContext) -> bool {
+        false
+    }
     fn contains_focused_view(&self, _: &AppContext) -> bool {
         false
     }
@@ -53,20 +54,27 @@ impl From<&dyn SidebarItemHandle> for AnyViewHandle {
 }
 
 pub struct Sidebar {
-    side: Side,
+    sidebar_side: SidebarSide,
     items: Vec<Item>,
     is_open: bool,
     active_item_ix: usize,
-    actual_width: Rc<RefCell<f32>>,
-    custom_width: Rc<RefCell<f32>>,
 }
 
 #[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
-pub enum Side {
+pub enum SidebarSide {
     Left,
     Right,
 }
 
+impl SidebarSide {
+    fn to_resizable_side(self) -> Side {
+        match self {
+            Self::Left => Side::Right,
+            Self::Right => Side::Left,
+        }
+    }
+}
+
 struct Item {
     icon_path: &'static str,
     tooltip: String,
@@ -80,21 +88,19 @@ pub struct SidebarButtons {
 
 #[derive(Clone, Debug, Deserialize, PartialEq)]
 pub struct ToggleSidebarItem {
-    pub side: Side,
+    pub sidebar_side: SidebarSide,
     pub item_index: usize,
 }
 
 impl_actions!(workspace, [ToggleSidebarItem]);
 
 impl Sidebar {
-    pub fn new(side: Side) -> Self {
+    pub fn new(sidebar_side: SidebarSide) -> Self {
         Self {
-            side,
+            sidebar_side,
             items: Default::default(),
             active_item_ix: 0,
             is_open: false,
-            actual_width: Rc::new(RefCell::new(260.)),
-            custom_width: Rc::new(RefCell::new(260.)),
         }
     }
 
@@ -171,38 +177,6 @@ impl Sidebar {
             None
         }
     }
-
-    fn render_resize_handle(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
-        let actual_width = self.actual_width.clone();
-        let custom_width = self.custom_width.clone();
-        let side = self.side;
-        MouseEventHandler::new::<Self, _, _>(side as usize, cx, |_, _| {
-            Empty::new()
-                .contained()
-                .with_style(theme.workspace.sidebar_resize_handle)
-                .boxed()
-        })
-        .with_padding(Padding {
-            left: 4.,
-            right: 4.,
-            ..Default::default()
-        })
-        .with_cursor_style(CursorStyle::ResizeLeftRight)
-        .on_down(MouseButton::Left, |_, _| {}) // This prevents the mouse down event from being propagated elsewhere
-        .on_drag(MouseButton::Left, move |e, cx| {
-            let delta = e.position.x() - e.prev_mouse_position.x();
-            let prev_width = *actual_width.borrow();
-            *custom_width.borrow_mut() = 0f32
-                .max(match side {
-                    Side::Left => prev_width + delta,
-                    Side::Right => prev_width - delta,
-                })
-                .round();
-
-            cx.notify();
-        })
-        .boxed()
-    }
 }
 
 impl Entity for Sidebar {
@@ -215,31 +189,20 @@ impl View for Sidebar {
     }
 
     fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
-        let theme = cx.global::<Settings>().theme.clone();
         if let Some(active_item) = self.active_item() {
-            let mut container = Flex::row();
-            if matches!(self.side, Side::Right) {
-                container.add_child(self.render_resize_handle(&theme, cx));
-            }
-
-            container.add_child(
-                Hook::new(
-                    ChildView::new(active_item.to_any())
-                        .constrained()
-                        .with_max_width(*self.custom_width.borrow())
-                        .boxed(),
+            enum ResizeHandleTag {}
+            let style = &cx.global::<Settings>().theme.workspace.sidebar;
+            ChildView::new(active_item.to_any())
+                .contained()
+                .with_style(style.container)
+                .with_resize_handle::<ResizeHandleTag, _>(
+                    self.sidebar_side as usize,
+                    self.sidebar_side.to_resizable_side(),
+                    4.,
+                    style.initial_size,
+                    cx,
                 )
-                .on_after_layout({
-                    let actual_width = self.actual_width.clone();
-                    move |size, _| *actual_width.borrow_mut() = size.x()
-                })
-                .flex(1., false)
-                .boxed(),
-            );
-            if matches!(self.side, Side::Left) {
-                container.add_child(self.render_resize_handle(&theme, cx));
-            }
-            container.boxed()
+                .boxed()
         } else {
             Empty::new().boxed()
         }
@@ -271,10 +234,10 @@ impl View for SidebarButtons {
         let badge_style = theme.badge;
         let active_ix = sidebar.active_item_ix;
         let is_open = sidebar.is_open;
-        let side = sidebar.side;
-        let group_style = match side {
-            Side::Left => theme.group_left,
-            Side::Right => theme.group_right,
+        let sidebar_side = sidebar.sidebar_side;
+        let group_style = match sidebar_side {
+            SidebarSide::Left => theme.group_left,
+            SidebarSide::Right => theme.group_right,
         };
 
         #[allow(clippy::needless_collect)]
@@ -288,10 +251,10 @@ impl View for SidebarButtons {
             .with_children(items.into_iter().enumerate().map(
                 |(ix, (icon_path, tooltip, item_view))| {
                     let action = ToggleSidebarItem {
-                        side,
+                        sidebar_side,
                         item_index: ix,
                     };
-                    MouseEventHandler::new::<Self, _, _>(ix, cx, move |state, cx| {
+                    MouseEventHandler::<Self>::new(ix, cx, move |state, cx| {
                         let is_active = is_open && ix == active_ix;
                         let style = item_style.style_for(state, is_active);
                         Stack::new()

crates/workspace/src/toolbar.rs 🔗

@@ -166,7 +166,7 @@ fn nav_button<A: Action + Clone>(
     action_name: &str,
     cx: &mut RenderContext<Toolbar>,
 ) -> ElementBox {
-    MouseEventHandler::new::<A, _, _>(0, cx, |state, _| {
+    MouseEventHandler::<A>::new(0, cx, |state, _| {
         let style = if enabled {
             style.style_for(state, false)
         } else {

crates/workspace/src/waiting_room.rs 🔗

@@ -1,4 +1,4 @@
-use crate::{sidebar::Side, AppState, ToggleFollow, Workspace};
+use crate::{sidebar::SidebarSide, AppState, ToggleFollow, Workspace};
 use anyhow::Result;
 use client::{proto, Client, Contact};
 use gpui::{
@@ -74,82 +74,84 @@ impl WaitingRoom {
     ) -> Self {
         let project_id = contact.projects[project_index].id;
         let client = app_state.client.clone();
-        let _join_task =
-            cx.spawn_weak({
-                let contact = contact.clone();
-                |this, mut cx| async move {
-                    let project = Project::remote(
-                        project_id,
-                        app_state.client.clone(),
-                        app_state.user_store.clone(),
-                        app_state.project_store.clone(),
-                        app_state.languages.clone(),
-                        app_state.fs.clone(),
-                        cx.clone(),
-                    )
-                    .await;
+        let _join_task = cx.spawn_weak({
+            let contact = contact.clone();
+            |this, mut cx| async move {
+                let project = Project::remote(
+                    project_id,
+                    app_state.client.clone(),
+                    app_state.user_store.clone(),
+                    app_state.project_store.clone(),
+                    app_state.languages.clone(),
+                    app_state.fs.clone(),
+                    cx.clone(),
+                )
+                .await;
 
-                    if let Some(this) = this.upgrade(&cx) {
-                        this.update(&mut cx, |this, cx| {
-                            this.waiting = false;
-                            match project {
-                                Ok(project) => {
-                                    cx.replace_root_view(|cx| {
-                                        let mut workspace = Workspace::new(project, cx);
-                                        (app_state.initialize_workspace)(
-                                            &mut workspace,
-                                            &app_state,
-                                            cx,
-                                        );
-                                        workspace.toggle_sidebar(Side::Left, cx);
-                                        if let Some((host_peer_id, _)) =
-                                            workspace.project.read(cx).collaborators().iter().find(
-                                                |(_, collaborator)| collaborator.replica_id == 0,
-                                            )
+                if let Some(this) = this.upgrade(&cx) {
+                    this.update(&mut cx, |this, cx| {
+                        this.waiting = false;
+                        match project {
+                            Ok(project) => {
+                                cx.replace_root_view(|cx| {
+                                    let mut workspace =
+                                        Workspace::new(project, app_state.default_item_factory, cx);
+                                    (app_state.initialize_workspace)(
+                                        &mut workspace,
+                                        &app_state,
+                                        cx,
+                                    );
+                                    workspace.toggle_sidebar(SidebarSide::Left, cx);
+                                    if let Some((host_peer_id, _)) = workspace
+                                        .project
+                                        .read(cx)
+                                        .collaborators()
+                                        .iter()
+                                        .find(|(_, collaborator)| collaborator.replica_id == 0)
+                                    {
+                                        if let Some(follow) = workspace
+                                            .toggle_follow(&ToggleFollow(*host_peer_id), cx)
                                         {
-                                            if let Some(follow) = workspace
-                                                .toggle_follow(&ToggleFollow(*host_peer_id), cx)
-                                            {
-                                                follow.detach_and_log_err(cx);
-                                            }
-                                        }
-                                        workspace
-                                    });
-                                }
-                                Err(error) => {
-                                    let login = &contact.user.github_login;
-                                    let message = match error {
-                                        project::JoinProjectError::HostDeclined => {
-                                            format!("@{} declined your request.", login)
+                                            follow.detach_and_log_err(cx);
                                         }
-                                        project::JoinProjectError::HostClosedProject => {
-                                            format!(
-                                                "@{} closed their copy of {}.",
-                                                login,
-                                                humanize_list(
-                                                    &contact.projects[project_index]
-                                                        .visible_worktree_root_names
-                                                )
+                                    }
+                                    workspace
+                                });
+                            }
+                            Err(error) => {
+                                let login = &contact.user.github_login;
+                                let message = match error {
+                                    project::JoinProjectError::HostDeclined => {
+                                        format!("@{} declined your request.", login)
+                                    }
+                                    project::JoinProjectError::HostClosedProject => {
+                                        format!(
+                                            "@{} closed their copy of {}.",
+                                            login,
+                                            humanize_list(
+                                                &contact.projects[project_index]
+                                                    .visible_worktree_root_names
                                             )
-                                        }
-                                        project::JoinProjectError::HostWentOffline => {
-                                            format!("@{} went offline.", login)
-                                        }
-                                        project::JoinProjectError::Other(error) => {
-                                            log::error!("error joining project: {}", error);
-                                            "An error occurred.".to_string()
-                                        }
-                                    };
-                                    this.message = message;
-                                    cx.notify();
-                                }
+                                        )
+                                    }
+                                    project::JoinProjectError::HostWentOffline => {
+                                        format!("@{} went offline.", login)
+                                    }
+                                    project::JoinProjectError::Other(error) => {
+                                        log::error!("error joining project: {}", error);
+                                        "An error occurred.".to_string()
+                                    }
+                                };
+                                this.message = message;
+                                cx.notify();
                             }
-                        })
-                    }
-
-                    Ok(())
+                        }
+                    })
                 }
-            });
+
+                Ok(())
+            }
+        });
 
         Self {
             project_id,

crates/workspace/src/workspace.rs 🔗

@@ -3,9 +3,9 @@
 ///
 /// This may cause issues when you're trying to write tests that use workspace focus to add items at
 /// specific locations.
+pub mod dock;
 pub mod pane;
 pub mod pane_group;
-pub mod programs;
 pub mod searchable;
 pub mod sidebar;
 mod status_bar;
@@ -18,6 +18,7 @@ use client::{
 };
 use clock::ReplicaId;
 use collections::{hash_map, HashMap, HashSet};
+use dock::{DefaultItemFactory, Dock, ToggleDockButton};
 use drag_and_drop::DragAndDrop;
 use futures::{channel::oneshot, FutureExt};
 use gpui::{
@@ -33,16 +34,15 @@ use gpui::{
     RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle,
 };
 use language::LanguageRegistry;
-use log::error;
+use log::{error, warn};
 pub use pane::*;
 pub use pane_group::*;
 use postage::prelude::Stream;
-use programs::ProgramManager;
 use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, ProjectStore, Worktree, WorktreeId};
 use searchable::SearchableItemHandle;
 use serde::Deserialize;
-use settings::{Autosave, Settings};
-use sidebar::{Side, Sidebar, SidebarButtons, ToggleSidebarItem};
+use settings::{Autosave, DockAnchor, Settings};
+use sidebar::{Sidebar, SidebarButtons, SidebarSide, ToggleSidebarItem};
 use smallvec::SmallVec;
 use status_bar::StatusBar;
 pub use status_bar::StatusItemView;
@@ -146,10 +146,8 @@ impl_internal_actions!(
 impl_actions!(workspace, [ToggleProjectOnline, ActivatePane]);
 
 pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
-    // Initialize the program manager immediately
-    cx.set_global(ProgramManager::new());
-
     pane::init(cx);
+    dock::init(cx);
 
     cx.add_global_action(open);
     cx.add_global_action({
@@ -217,10 +215,10 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
         workspace.activate_next_pane(cx)
     });
     cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftSidebar, cx| {
-        workspace.toggle_sidebar(Side::Left, cx);
+        workspace.toggle_sidebar(SidebarSide::Left, cx);
     });
     cx.add_action(|workspace: &mut Workspace, _: &ToggleRightSidebar, cx| {
-        workspace.toggle_sidebar(Side::Right, cx);
+        workspace.toggle_sidebar(SidebarSide::Right, cx);
     });
     cx.add_action(Workspace::activate_pane_at_index);
 
@@ -265,6 +263,7 @@ pub struct AppState {
     pub fs: Arc<dyn fs::Fs>,
     pub build_window_options: fn() -> WindowOptions<'static>,
     pub initialize_workspace: fn(&mut Workspace, &Arc<AppState>, &mut ViewContext<Workspace>),
+    pub default_item_factory: DefaultItemFactory,
 }
 
 #[derive(Eq, PartialEq, Hash)]
@@ -870,11 +869,13 @@ impl AppState {
             project_store,
             initialize_workspace: |_, _, _| {},
             build_window_options: Default::default,
+            default_item_factory: |_, _| unimplemented!(),
         })
     }
 }
 
 pub enum Event {
+    DockAnchorChanged,
     PaneAdded(ViewHandle<Pane>),
     ContactRequestedJoin(u64),
 }
@@ -892,7 +893,9 @@ pub struct Workspace {
     panes: Vec<ViewHandle<Pane>>,
     panes_by_item: HashMap<usize, WeakViewHandle<Pane>>,
     active_pane: ViewHandle<Pane>,
+    last_active_center_pane: Option<ViewHandle<Pane>>,
     status_bar: ViewHandle<StatusBar>,
+    dock: Dock,
     notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
     project: ModelHandle<Project>,
     leader_state: LeaderState,
@@ -922,7 +925,11 @@ enum FollowerItem {
 }
 
 impl Workspace {
-    pub fn new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
+    pub fn new(
+        project: ModelHandle<Project>,
+        dock_default_factory: DefaultItemFactory,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
         cx.observe_fullscreen(|_, _, cx| cx.notify()).detach();
 
         cx.observe_window_activation(Self::on_window_activation_changed)
@@ -949,14 +956,14 @@ impl Workspace {
         })
         .detach();
 
-        let pane = cx.add_view(Pane::new);
-        let pane_id = pane.id();
-        cx.subscribe(&pane, move |this, _, event, cx| {
+        let center_pane = cx.add_view(|cx| Pane::new(None, cx));
+        let pane_id = center_pane.id();
+        cx.subscribe(&center_pane, move |this, _, event, cx| {
             this.handle_pane_event(pane_id, event, cx)
         })
         .detach();
-        cx.focus(&pane);
-        cx.emit(Event::PaneAdded(pane.clone()));
+        cx.focus(&center_pane);
+        cx.emit(Event::PaneAdded(center_pane.clone()));
 
         let fs = project.read(cx).fs().clone();
         let user_store = project.read(cx).user_store();
@@ -978,33 +985,44 @@ impl Workspace {
             }
         });
 
-        let weak_self = cx.weak_handle();
+        let handle = cx.handle();
+        let weak_handle = cx.weak_handle();
 
-        cx.emit_global(WorkspaceCreated(weak_self.clone()));
+        cx.emit_global(WorkspaceCreated(weak_handle.clone()));
 
-        let left_sidebar = cx.add_view(|_| Sidebar::new(Side::Left));
-        let right_sidebar = cx.add_view(|_| Sidebar::new(Side::Right));
+        let dock = Dock::new(cx, dock_default_factory);
+        let dock_pane = dock.pane().clone();
+
+        let left_sidebar = cx.add_view(|_| Sidebar::new(SidebarSide::Left));
+        let right_sidebar = cx.add_view(|_| Sidebar::new(SidebarSide::Right));
         let left_sidebar_buttons = cx.add_view(|cx| SidebarButtons::new(left_sidebar.clone(), cx));
+        let toggle_dock = cx.add_view(|cx| ToggleDockButton::new(handle, cx));
         let right_sidebar_buttons =
             cx.add_view(|cx| SidebarButtons::new(right_sidebar.clone(), cx));
         let status_bar = cx.add_view(|cx| {
-            let mut status_bar = StatusBar::new(&pane.clone(), cx);
+            let mut status_bar = StatusBar::new(&center_pane.clone(), cx);
             status_bar.add_left_item(left_sidebar_buttons, cx);
             status_bar.add_right_item(right_sidebar_buttons, cx);
+            status_bar.add_right_item(toggle_dock, cx);
             status_bar
         });
 
         cx.update_default_global::<DragAndDrop<Workspace>, _, _>(|drag_and_drop, _| {
-            drag_and_drop.register_container(weak_self.clone());
+            drag_and_drop.register_container(weak_handle.clone());
         });
 
         let mut this = Workspace {
             modal: None,
-            weak_self,
-            center: PaneGroup::new(pane.clone()),
-            panes: vec![pane.clone()],
+            weak_self: weak_handle,
+            center: PaneGroup::new(center_pane.clone()),
+            dock,
+            // When removing an item, the last element remaining in this array
+            // is used to find where focus should fallback to. As such, the order
+            // of these two variables is important.
+            panes: vec![dock_pane, center_pane.clone()],
             panes_by_item: Default::default(),
-            active_pane: pane.clone(),
+            active_pane: center_pane.clone(),
+            last_active_center_pane: Some(center_pane.clone()),
             status_bar,
             notifications: Default::default(),
             client,
@@ -1078,6 +1096,7 @@ impl Workspace {
                         app_state.fs.clone(),
                         cx,
                     ),
+                    app_state.default_item_factory,
                     cx,
                 );
                 (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
@@ -1459,24 +1478,31 @@ impl Workspace {
         }
     }
 
-    pub fn toggle_sidebar(&mut self, side: Side, cx: &mut ViewContext<Self>) {
-        let sidebar = match side {
-            Side::Left => &mut self.left_sidebar,
-            Side::Right => &mut self.right_sidebar,
+    pub fn toggle_sidebar(&mut self, sidebar_side: SidebarSide, cx: &mut ViewContext<Self>) {
+        let sidebar = match sidebar_side {
+            SidebarSide::Left => &mut self.left_sidebar,
+            SidebarSide::Right => &mut self.right_sidebar,
         };
-        sidebar.update(cx, |sidebar, cx| {
-            sidebar.set_open(!sidebar.is_open(), cx);
+        let open = sidebar.update(cx, |sidebar, cx| {
+            let open = !sidebar.is_open();
+            sidebar.set_open(open, cx);
+            open
         });
+
+        if open {
+            Dock::hide_on_sidebar_shown(self, sidebar_side, cx);
+        }
+
         cx.focus_self();
         cx.notify();
     }
 
     pub fn toggle_sidebar_item(&mut self, action: &ToggleSidebarItem, cx: &mut ViewContext<Self>) {
-        let sidebar = match action.side {
-            Side::Left => &mut self.left_sidebar,
-            Side::Right => &mut self.right_sidebar,
+        let sidebar = match action.sidebar_side {
+            SidebarSide::Left => &mut self.left_sidebar,
+            SidebarSide::Right => &mut self.right_sidebar,
         };
-        let active_item = sidebar.update(cx, |sidebar, cx| {
+        let active_item = sidebar.update(cx, move |sidebar, cx| {
             if sidebar.is_open() && sidebar.active_item_ix() == action.item_index {
                 sidebar.set_open(false, cx);
                 None
@@ -1486,7 +1512,10 @@ impl Workspace {
                 sidebar.active_item().cloned()
             }
         });
+
         if let Some(active_item) = active_item {
+            Dock::hide_on_sidebar_shown(self, action.sidebar_side, cx);
+
             if active_item.is_focused(cx) {
                 cx.focus_self();
             } else {
@@ -1500,13 +1529,13 @@ impl Workspace {
 
     pub fn toggle_sidebar_item_focus(
         &mut self,
-        side: Side,
+        sidebar_side: SidebarSide,
         item_index: usize,
         cx: &mut ViewContext<Self>,
     ) {
-        let sidebar = match side {
-            Side::Left => &mut self.left_sidebar,
-            Side::Right => &mut self.right_sidebar,
+        let sidebar = match sidebar_side {
+            SidebarSide::Left => &mut self.left_sidebar,
+            SidebarSide::Right => &mut self.right_sidebar,
         };
         let active_item = sidebar.update(cx, |sidebar, cx| {
             sidebar.set_open(true, cx);
@@ -1514,6 +1543,8 @@ impl Workspace {
             sidebar.active_item().cloned()
         });
         if let Some(active_item) = active_item {
+            Dock::hide_on_sidebar_shown(self, sidebar_side, cx);
+
             if active_item.is_focused(cx) {
                 cx.focus_self();
             } else {
@@ -1529,7 +1560,7 @@ impl Workspace {
     }
 
     fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
-        let pane = cx.add_view(Pane::new);
+        let pane = cx.add_view(|cx| Pane::new(None, cx));
         let pane_id = pane.id();
         cx.subscribe(&pane, move |this, _, event, cx| {
             this.handle_pane_event(pane_id, event, cx)
@@ -1682,6 +1713,15 @@ impl Workspace {
                 status_bar.set_active_pane(&self.active_pane, cx);
             });
             self.active_item_path_changed(cx);
+
+            if &pane == self.dock_pane() {
+                Dock::show(self, cx);
+            } else {
+                self.last_active_center_pane = Some(pane.clone());
+                if self.dock.is_anchored_at(DockAnchor::Expanded) {
+                    Dock::hide(self, cx);
+                }
+            }
             cx.notify();
         }
 
@@ -1701,21 +1741,19 @@ impl Workspace {
         cx: &mut ViewContext<Self>,
     ) {
         if let Some(pane) = self.pane(pane_id) {
+            let is_dock = &pane == self.dock.pane();
             match event {
-                pane::Event::Split(direction) => {
+                pane::Event::Split(direction) if !is_dock => {
                     self.split_pane(pane, *direction, cx);
                 }
-                pane::Event::Remove => {
-                    self.remove_pane(pane, cx);
-                }
-                pane::Event::Focused => {
-                    self.handle_pane_focused(pane, cx);
-                }
+                pane::Event::Remove if !is_dock => self.remove_pane(pane, cx),
+                pane::Event::Remove if is_dock => Dock::hide(self, cx),
+                pane::Event::Focused => self.handle_pane_focused(pane, cx),
                 pane::Event::ActivateItem { local } => {
                     if *local {
                         self.unfollow(&pane, cx);
                     }
-                    if pane == self.active_pane {
+                    if &pane == self.active_pane() {
                         self.active_item_path_changed(cx);
                     }
                 }
@@ -1733,8 +1771,9 @@ impl Workspace {
                         }
                     }
                 }
+                _ => {}
             }
-        } else {
+        } else if self.dock.visible_pane().is_none() {
             error!("pane {} not found", pane_id);
         }
     }
@@ -1745,6 +1784,11 @@ impl Workspace {
         direction: SplitDirection,
         cx: &mut ViewContext<Self>,
     ) -> Option<ViewHandle<Pane>> {
+        if &pane == self.dock_pane() {
+            warn!("Can't split dock pane.");
+            return None;
+        }
+
         pane.read(cx).active_item().map(|item| {
             let new_pane = self.add_pane(cx);
             if let Some(clone) = item.clone_on_split(cx.as_mut()) {
@@ -1765,6 +1809,10 @@ impl Workspace {
             for removed_item in pane.read(cx).items() {
                 self.panes_by_item.remove(&removed_item.id());
             }
+            if self.last_active_center_pane == Some(pane) {
+                self.last_active_center_pane = None;
+            }
+
             cx.notify();
         } else {
             self.active_item_path_changed(cx);
@@ -1783,6 +1831,10 @@ impl Workspace {
         &self.active_pane
     }
 
+    pub fn dock_pane(&self) -> &ViewHandle<Pane> {
+        self.dock.pane()
+    }
+
     fn project_remote_id_changed(&mut self, remote_id: Option<u64>, cx: &mut ViewContext<Self>) {
         if let Some(remote_id) = remote_id {
             self.remote_entity_subscription =
@@ -1975,8 +2027,9 @@ impl Workspace {
             theme.workspace.titlebar.container
         };
 
+        enum TitleBar {}
         ConstrainedBox::new(
-            MouseEventHandler::new::<Self, _, _>(0, cx, |_, cx| {
+            MouseEventHandler::<TitleBar>::new(0, cx, |_, cx| {
                 Container::new(
                     Stack::new()
                         .with_child(
@@ -2105,7 +2158,7 @@ impl Workspace {
             None
         } else {
             Some(
-                MouseEventHandler::new::<Authenticate, _, _>(0, cx, |state, _| {
+                MouseEventHandler::<Authenticate>::new(0, cx, |state, _| {
                     let style = theme
                         .workspace
                         .titlebar
@@ -2165,7 +2218,7 @@ impl Workspace {
             .boxed();
 
         if let Some((peer_id, peer_github_login)) = peer {
-            MouseEventHandler::new::<ToggleFollow, _, _>(replica_id.into(), cx, move |_, _| content)
+            MouseEventHandler::<ToggleFollow>::new(replica_id.into(), cx, move |_, _| content)
                 .with_cursor_style(CursorStyle::PointingHand)
                 .on_click(MouseButton::Left, move |_, cx| {
                     cx.dispatch_action(ToggleFollow(peer_id))
@@ -2191,7 +2244,7 @@ impl Workspace {
         if self.project.read(cx).is_read_only() {
             enum DisconnectedOverlay {}
             Some(
-                MouseEventHandler::new::<DisconnectedOverlay, _, _>(0, cx, |_, cx| {
+                MouseEventHandler::<DisconnectedOverlay>::new(0, cx, |_, cx| {
                     let theme = &cx.global::<Settings>().theme;
                     Label::new(
                         "Your connection to the remote project has been lost.".to_string(),
@@ -2202,6 +2255,7 @@ impl Workspace {
                     .with_style(theme.workspace.disconnected_overlay.container)
                     .boxed()
                 })
+                .with_cursor_style(CursorStyle::Arrow)
                 .capture_all()
                 .boxed(),
             )
@@ -2557,14 +2611,28 @@ impl View for Workspace {
                                         },
                                     )
                                     .with_child(
-                                        FlexItem::new(self.center.render(
-                                            &theme,
-                                            &self.follower_states_by_leader,
-                                            self.project.read(cx).collaborators(),
-                                        ))
+                                        FlexItem::new(
+                                            Flex::column()
+                                                .with_child(
+                                                    FlexItem::new(self.center.render(
+                                                        &theme,
+                                                        &self.follower_states_by_leader,
+                                                        self.project.read(cx).collaborators(),
+                                                    ))
+                                                    .flex(1., true)
+                                                    .boxed(),
+                                                )
+                                                .with_children(self.dock.render(
+                                                    &theme,
+                                                    DockAnchor::Bottom,
+                                                    cx,
+                                                ))
+                                                .boxed(),
+                                        )
                                         .flex(1., true)
                                         .boxed(),
                                     )
+                                    .with_children(self.dock.render(&theme, DockAnchor::Right, cx))
                                     .with_children(
                                         if self.right_sidebar.read(cx).active_item().is_some() {
                                             Some(
@@ -2578,15 +2646,27 @@ impl View for Workspace {
                                     )
                                     .boxed()
                             })
-                            .with_children(self.modal.as_ref().map(|m| {
-                                ChildView::new(m)
-                                    .contained()
-                                    .with_style(theme.workspace.modal)
-                                    .aligned()
-                                    .top()
-                                    .boxed()
-                            }))
-                            .with_children(self.render_notifications(&theme.workspace))
+                            .with_child(
+                                Overlay::new(
+                                    Stack::new()
+                                        .with_children(self.dock.render(
+                                            &theme,
+                                            DockAnchor::Expanded,
+                                            cx,
+                                        ))
+                                        .with_children(self.modal.as_ref().map(|m| {
+                                            ChildView::new(m)
+                                                .contained()
+                                                .with_style(theme.workspace.modal)
+                                                .aligned()
+                                                .top()
+                                                .boxed()
+                                        }))
+                                        .with_children(self.render_notifications(&theme.workspace))
+                                        .boxed(),
+                                )
+                                .boxed(),
+                            )
                             .flex(1.0, true)
                             .boxed(),
                     )
@@ -2605,6 +2685,14 @@ impl View for Workspace {
             cx.focus(&self.active_pane);
         }
     }
+
+    fn keymap_context(&self, _: &AppContext) -> gpui::keymap::Context {
+        let mut keymap = Self::default_keymap_context();
+        if self.active_pane() == self.dock_pane() {
+            keymap.set.insert("Dock".into());
+        }
+        keymap
+    }
 }
 
 pub trait WorkspaceHandle {
@@ -2785,10 +2873,10 @@ pub fn open_paths(
                     cx,
                 );
                 new_project = Some(project.clone());
-                let mut workspace = Workspace::new(project, cx);
+                let mut workspace = Workspace::new(project, app_state.default_item_factory, cx);
                 (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
                 if contains_directory {
-                    workspace.toggle_sidebar(Side::Left, cx);
+                    workspace.toggle_sidebar(SidebarSide::Left, cx);
                 }
                 workspace
             })
@@ -2846,6 +2934,7 @@ fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
                 app_state.fs.clone(),
                 cx,
             ),
+            app_state.default_item_factory,
             cx,
         );
         (app_state.initialize_workspace)(&mut workspace, app_state, cx);
@@ -2858,11 +2947,20 @@ fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
 mod tests {
     use std::cell::Cell;
 
+    use crate::sidebar::SidebarItem;
+
     use super::*;
     use gpui::{executor::Deterministic, ModelHandle, TestAppContext, ViewContext};
     use project::{FakeFs, Project, ProjectEntryId};
     use serde_json::json;
 
+    pub fn default_item_factory(
+        _workspace: &mut Workspace,
+        _cx: &mut ViewContext<Workspace>,
+    ) -> Box<dyn ItemHandle> {
+        unimplemented!();
+    }
+
     #[gpui::test]
     async fn test_tab_disambiguation(cx: &mut TestAppContext) {
         cx.foreground().forbid_parking();
@@ -2870,7 +2968,8 @@ mod tests {
 
         let fs = FakeFs::new(cx.background());
         let project = Project::test(fs, [], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
+        let (_, workspace) =
+            cx.add_window(|cx| Workspace::new(project.clone(), default_item_factory, cx));
 
         // Adding an item with no ambiguity renders the tab without detail.
         let item1 = cx.add_view(&workspace, |_| {
@@ -2934,7 +3033,8 @@ mod tests {
         .await;
 
         let project = Project::test(fs, ["root1".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
+        let (window_id, workspace) =
+            cx.add_window(|cx| Workspace::new(project.clone(), default_item_factory, cx));
         let worktree_id = project.read_with(cx, |project, cx| {
             project.worktrees(cx).next().unwrap().read(cx).id()
         });
@@ -3030,7 +3130,8 @@ mod tests {
         fs.insert_tree("/root", json!({ "one": "" })).await;
 
         let project = Project::test(fs, ["root".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
+        let (window_id, workspace) =
+            cx.add_window(|cx| Workspace::new(project.clone(), default_item_factory, cx));
 
         // When there are no dirty items, there's nothing to do.
         let item1 = cx.add_view(&workspace, |_| TestItem::new());
@@ -3070,7 +3171,8 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+        let (window_id, workspace) =
+            cx.add_window(|cx| Workspace::new(project, default_item_factory, cx));
 
         let item1 = cx.add_view(&workspace, |_| {
             let mut item = TestItem::new();
@@ -3165,7 +3267,8 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, [], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+        let (window_id, workspace) =
+            cx.add_window(|cx| Workspace::new(project, default_item_factory, cx));
 
         // Create several workspace items with single project entries, and two
         // workspace items with multiple project entries.
@@ -3266,7 +3369,8 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, [], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+        let (window_id, workspace) =
+            cx.add_window(|cx| Workspace::new(project, default_item_factory, cx));
 
         let item = cx.add_view(&workspace, |_| {
             let mut item = TestItem::new();
@@ -3383,7 +3487,7 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, [], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+        let (_, workspace) = cx.add_window(|cx| Workspace::new(project, default_item_factory, cx));
 
         let item = cx.add_view(&workspace, |_| {
             let mut item = TestItem::new();
@@ -3635,4 +3739,6 @@ mod tests {
             vec![ItemEvent::UpdateTab, ItemEvent::Edit]
         }
     }
+
+    impl SidebarItem for TestItem {}
 }

crates/zed/Cargo.toml 🔗

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
 description = "The fast, collaborative code editor."
 edition = "2021"
 name = "zed"
-version = "0.53.1"
+version = "0.54.1"
 
 [lib]
 name = "zed"
@@ -27,6 +27,7 @@ context_menu = { path = "../context_menu" }
 client = { path = "../client" }
 clock = { path = "../clock" }
 contacts_panel = { path = "../contacts_panel" }
+contacts_status_item = { path = "../contacts_status_item" }
 diagnostics = { path = "../diagnostics" }
 editor = { path = "../editor" }
 file_finder = { path = "../file_finder" }

crates/zed/src/feedback.rs 🔗

@@ -21,7 +21,7 @@ impl View for FeedbackLink {
     }
 
     fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> gpui::ElementBox {
-        MouseEventHandler::new::<Self, _, _>(0, cx, |state, cx| {
+        MouseEventHandler::<Self>::new(0, cx, |state, cx| {
             let theme = &cx.global::<Settings>().theme;
             let theme = &theme.workspace.status_bar.feedback;
             Text::new(

crates/zed/src/languages/json.rs 🔗

@@ -1,26 +1,22 @@
-use super::installation::{npm_install_packages, npm_package_latest_version};
-use anyhow::{anyhow, Context, Result};
+use super::installation::{latest_github_release, GitHubLspBinaryVersion};
+use anyhow::{anyhow, Result};
+use async_compression::futures::bufread::GzipDecoder;
 use async_trait::async_trait;
 use client::http::HttpClient;
 use collections::HashMap;
-use futures::StreamExt;
+use futures::{io::BufReader, StreamExt};
 use language::{LanguageServerName, LspAdapter};
 use serde_json::json;
-use smol::fs;
-use std::{any::Any, path::PathBuf, sync::Arc};
+use smol::fs::{self, File};
+use std::{any::Any, env::consts, path::PathBuf, sync::Arc};
 use util::ResultExt;
 
 pub struct JsonLspAdapter;
 
-impl JsonLspAdapter {
-    const BIN_PATH: &'static str =
-        "node_modules/vscode-json-languageserver/bin/vscode-json-languageserver";
-}
-
 #[async_trait]
 impl LspAdapter for JsonLspAdapter {
     async fn name(&self) -> LanguageServerName {
-        LanguageServerName("vscode-json-languageserver".into())
+        LanguageServerName("json-language-server".into())
     }
 
     async fn server_args(&self) -> Vec<String> {
@@ -29,28 +25,46 @@ impl LspAdapter for JsonLspAdapter {
 
     async fn fetch_latest_server_version(
         &self,
-        _: Arc<dyn HttpClient>,
-    ) -> Result<Box<dyn 'static + Any + Send>> {
-        Ok(Box::new(npm_package_latest_version("vscode-json-languageserver").await?) as Box<_>)
+        http: Arc<dyn HttpClient>,
+    ) -> Result<Box<dyn 'static + Send + Any>> {
+        let release = latest_github_release("zed-industries/json-language-server", http).await?;
+        let asset_name = format!("json-language-server-darwin-{}.gz", consts::ARCH);
+        let asset = release
+            .assets
+            .iter()
+            .find(|asset| asset.name == asset_name)
+            .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
+        let version = GitHubLspBinaryVersion {
+            name: release.name,
+            url: asset.browser_download_url.clone(),
+        };
+        Ok(Box::new(version) as Box<_>)
     }
 
     async fn fetch_server_binary(
         &self,
         version: Box<dyn 'static + Send + Any>,
-        _: Arc<dyn HttpClient>,
+        http: Arc<dyn HttpClient>,
         container_dir: PathBuf,
     ) -> Result<PathBuf> {
-        let version = version.downcast::<String>().unwrap();
-        let version_dir = container_dir.join(version.as_str());
-        fs::create_dir_all(&version_dir)
-            .await
-            .context("failed to create version directory")?;
-        let binary_path = version_dir.join(Self::BIN_PATH);
+        let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
+        let destination_path = container_dir.join(format!(
+            "json-language-server-{}-{}",
+            version.name,
+            consts::ARCH
+        ));
 
-        if fs::metadata(&binary_path).await.is_err() {
-            npm_install_packages(
-                [("vscode-json-languageserver", version.as_str())],
-                &version_dir,
+        if fs::metadata(&destination_path).await.is_err() {
+            let mut response = http
+                .get(&version.url, Default::default(), true)
+                .await
+                .map_err(|err| anyhow!("error downloading release: {}", err))?;
+            let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
+            let mut file = File::create(&destination_path).await?;
+            futures::io::copy(decompressed_bytes, &mut file).await?;
+            fs::set_permissions(
+                &destination_path,
+                <fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
             )
             .await?;
 
@@ -58,37 +72,25 @@ impl LspAdapter for JsonLspAdapter {
                 while let Some(entry) = entries.next().await {
                     if let Some(entry) = entry.log_err() {
                         let entry_path = entry.path();
-                        if entry_path.as_path() != version_dir {
-                            fs::remove_dir_all(&entry_path).await.log_err();
+                        if entry_path.as_path() != destination_path {
+                            fs::remove_file(&entry_path).await.log_err();
                         }
                     }
                 }
             }
         }
 
-        Ok(binary_path)
+        Ok(destination_path)
     }
 
     async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<PathBuf> {
         (|| async move {
-            let mut last_version_dir = None;
+            let mut last = None;
             let mut entries = fs::read_dir(&container_dir).await?;
             while let Some(entry) = entries.next().await {
-                let entry = entry?;
-                if entry.file_type().await?.is_dir() {
-                    last_version_dir = Some(entry.path());
-                }
-            }
-            let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
-            let bin_path = last_version_dir.join(Self::BIN_PATH);
-            if bin_path.exists() {
-                Ok(bin_path)
-            } else {
-                Err(anyhow!(
-                    "missing executable in directory {:?}",
-                    last_version_dir
-                ))
+                last = Some(entry?.path());
             }
+            last.ok_or_else(|| anyhow!("no cached binary"))
         })()
         .await
         .log_err()

crates/zed/src/main.rs 🔗

@@ -19,20 +19,21 @@ use futures::{
     channel::{mpsc, oneshot},
     FutureExt, SinkExt, StreamExt,
 };
-use gpui::{executor::Background, App, AssetSource, AsyncAppContext, Task};
+use gpui::{executor::Background, App, AssetSource, AsyncAppContext, Task, ViewContext};
 use isahc::{config::Configurable, AsyncBody, Request};
 use language::LanguageRegistry;
 use log::LevelFilter;
 use parking_lot::Mutex;
 use project::{Fs, ProjectStore};
 use serde_json::json;
-use settings::{self, KeymapFileContent, Settings, SettingsFileContent};
+use settings::{self, KeymapFileContent, Settings, SettingsFileContent, WorkingDirectory};
 use smol::process::Command;
 use std::{env, ffi::OsStr, fs, panic, path::PathBuf, sync::Arc, thread, time::Duration};
+use terminal::terminal_container_view::{get_working_directory, TerminalContainer};
 
 use theme::ThemeRegistry;
 use util::{ResultExt, TryFutureExt};
-use workspace::{self, AppState, NewFile, OpenPaths};
+use workspace::{self, AppState, ItemHandle, NewFile, OpenPaths, Workspace};
 use zed::{
     self, build_window_options,
     fs::RealFs,
@@ -148,6 +149,7 @@ fn main() {
             fs,
             build_window_options,
             initialize_workspace,
+            default_item_factory,
         });
         auto_update::init(db, http, client::ZED_SERVER_URL.clone(), cx);
         workspace::init(app_state.clone(), cx);
@@ -591,3 +593,20 @@ async fn handle_cli_connection(
         }
     }
 }
+
+pub fn default_item_factory(
+    workspace: &mut Workspace,
+    cx: &mut ViewContext<Workspace>,
+) -> Box<dyn ItemHandle> {
+    let strategy = cx
+        .global::<Settings>()
+        .terminal_overrides
+        .working_directory
+        .clone()
+        .unwrap_or(WorkingDirectory::CurrentProjectDirectory);
+
+    let working_directory = get_working_directory(workspace, cx, strategy);
+
+    let terminal_handle = cx.add_view(|cx| TerminalContainer::new(working_directory, false, cx));
+    Box::new(terminal_handle)
+}

crates/zed/src/zed.rs 🔗

@@ -20,7 +20,7 @@ use gpui::{
     geometry::vector::vec2f,
     impl_actions,
     platform::{WindowBounds, WindowOptions},
-    AssetSource, AsyncAppContext, ViewContext,
+    AssetSource, AsyncAppContext, TitlebarOptions, ViewContext, WindowKind,
 };
 use language::Rope;
 pub use lsp;
@@ -33,7 +33,7 @@ use settings::{keymap_file_json_schema, settings_file_json_schema, Settings};
 use std::{env, path::Path, str, sync::Arc};
 use util::ResultExt;
 pub use workspace;
-use workspace::{sidebar::Side, AppState, Workspace};
+use workspace::{sidebar::SidebarSide, AppState, Workspace};
 
 #[derive(Deserialize, Clone, PartialEq)]
 struct OpenBrowser {
@@ -204,14 +204,14 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
         |workspace: &mut Workspace,
          _: &project_panel::ToggleFocus,
          cx: &mut ViewContext<Workspace>| {
-            workspace.toggle_sidebar_item_focus(Side::Left, 0, cx);
+            workspace.toggle_sidebar_item_focus(SidebarSide::Left, 0, cx);
         },
     );
     cx.add_action(
         |workspace: &mut Workspace,
          _: &contacts_panel::ToggleFocus,
          cx: &mut ViewContext<Workspace>| {
-            workspace.toggle_sidebar_item_focus(Side::Right, 0, cx);
+            workspace.toggle_sidebar_item_focus(SidebarSide::Right, 0, cx);
         },
     );
 
@@ -243,6 +243,7 @@ pub fn initialize_workspace(
     .detach();
 
     cx.emit(workspace::Event::PaneAdded(workspace.active_pane().clone()));
+    cx.emit(workspace::Event::PaneAdded(workspace.dock_pane().clone()));
 
     let settings = cx.global::<Settings>();
 
@@ -330,9 +331,14 @@ pub fn initialize_workspace(
 pub fn build_window_options() -> WindowOptions<'static> {
     WindowOptions {
         bounds: WindowBounds::Maximized,
-        title: None,
-        titlebar_appears_transparent: true,
-        traffic_light_position: Some(vec2f(8., 8.)),
+        titlebar: Some(TitlebarOptions {
+            title: None,
+            appears_transparent: true,
+            traffic_light_position: Some(vec2f(8., 8.)),
+        }),
+        center: false,
+        kind: WindowKind::Normal,
+        is_movable: true,
     }
 }
 
@@ -723,7 +729,8 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+        let (_, workspace) =
+            cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
 
         let entries = cx.read(|cx| workspace.file_project_paths(cx));
         let file1 = entries[0].clone();
@@ -842,7 +849,8 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/dir1".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+        let (_, workspace) =
+            cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
 
         // Open a file within an existing worktree.
         cx.update(|cx| {
@@ -1001,7 +1009,8 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+        let (window_id, workspace) =
+            cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
 
         // Open a file within an existing worktree.
         cx.update(|cx| {
@@ -1043,7 +1052,8 @@ mod tests {
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
         project.update(cx, |project, _| project.languages().add(rust_lang()));
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+        let (window_id, workspace) =
+            cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
         let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap());
 
         // Create a new untitled buffer
@@ -1132,7 +1142,8 @@ mod tests {
 
         let project = Project::test(app_state.fs.clone(), [], cx).await;
         project.update(cx, |project, _| project.languages().add(rust_lang()));
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+        let (window_id, workspace) =
+            cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
 
         // Create a new untitled buffer
         cx.dispatch_action(window_id, NewFile);
@@ -1185,7 +1196,8 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+        let (window_id, workspace) =
+            cx.add_window(|cx| Workspace::new(project, |_, _| unimplemented!(), cx));
 
         let entries = cx.read(|cx| workspace.file_project_paths(cx));
         let file1 = entries[0].clone();
@@ -1221,7 +1233,7 @@ mod tests {
 
         cx.foreground().run_until_parked();
         workspace.read_with(cx, |workspace, _| {
-            assert_eq!(workspace.panes().len(), 1);
+            assert_eq!(workspace.panes().len(), 2); //Center pane + Dock pane
             assert_eq!(workspace.active_pane(), &pane_1);
         });
 
@@ -1231,6 +1243,7 @@ mod tests {
         cx.foreground().run_until_parked();
 
         workspace.read_with(cx, |workspace, cx| {
+            assert_eq!(workspace.panes().len(), 2);
             assert!(workspace.active_item(cx).is_none());
         });
 
@@ -1258,7 +1271,8 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
+        let (_, workspace) =
+            cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
 
         let entries = cx.read(|cx| workspace.file_project_paths(cx));
         let file1 = entries[0].clone();
@@ -1522,7 +1536,8 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
+        let (_, workspace) =
+            cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
         let entries = cx.read(|cx| workspace.file_project_paths(cx));

script/bundle 🔗

@@ -3,6 +3,7 @@
 set -e
 
 export ZED_BUNDLE=true
+export MACOSX_DEPLOYMENT_TARGET=10.14
 
 echo "Installing cargo bundle"
 cargo install cargo-bundle --version 0.5.0

styles/package-lock.json 🔗

@@ -5,6 +5,7 @@
   "requires": true,
   "packages": {
     "": {
+      "name": "styles",
       "version": "1.0.0",
       "license": "ISC",
       "dependencies": {

styles/src/styleTree/app.ts 🔗

@@ -3,6 +3,7 @@ import chatPanel from "./chatPanel";
 import { text } from "./components";
 import contactFinder from "./contactFinder";
 import contactsPanel from "./contactsPanel";
+import contactsPopover from "./contactsPopover";
 import commandPalette from "./commandPalette";
 import editor from "./editor";
 import projectPanel from "./projectPanel";
@@ -34,6 +35,7 @@ export default function app(theme: Theme): Object {
     commandPalette: commandPalette(theme),
     projectPanel: projectPanel(theme),
     chatPanel: chatPanel(theme),
+    contactsPopover: contactsPopover(theme),
     contactsPanel: contactsPanel(theme),
     contactFinder: contactFinder(theme),
     search: search(theme),

styles/src/styleTree/contactsPopover.ts 🔗

@@ -0,0 +1,8 @@
+import Theme from "../themes/common/theme";
+import { backgroundColor } from "./components";
+
+export default function workspace(theme: Theme) {
+  return {
+    background: backgroundColor(theme, 300),
+  }
+}

styles/src/styleTree/search.ts 🔗

@@ -46,6 +46,11 @@ export default function search(theme: Theme) {
         background: backgroundColor(theme, "on500", "active"),
         border: border(theme, "muted"),
       },
+      clicked: {
+        ...text(theme, "mono", "active"),
+        background: backgroundColor(theme, "on300", "active"),
+        border: border(theme, "secondary"),
+      },
       hover: {
         ...text(theme, "mono", "active"),
         background: backgroundColor(theme, "on500", "hovered"),

styles/src/styleTree/tabBar.ts 🔗

@@ -59,13 +59,7 @@ export default function tabBar(theme: Theme) {
   const draggedTab = {
     ...activePaneActiveTab,
     background: withOpacity(tab.background, 0.8),
-    border: {
-      ...tab.border,
-      top: false,
-      left: false,
-      right: false,
-      bottom: false,
-    },
+    border: undefined as any, // Remove border
     shadow: draggedShadow(theme),
   }
 
@@ -74,7 +68,6 @@ export default function tabBar(theme: Theme) {
     background: backgroundColor(theme, 300),
     dropTargetOverlayColor: withOpacity(theme.textColor.muted, 0.6),
     border: border(theme, "primary", {
-      left: true,
       bottom: true,
       overlay: true,
     }),
@@ -89,15 +82,18 @@ export default function tabBar(theme: Theme) {
     draggedTab,
     paneButton: {
       color: iconColor(theme, "secondary"),
-      border: {
-        ...tab.border,
-      },
       iconWidth: 12,
       buttonWidth: activePaneActiveTab.height,
       hover: {
         color: iconColor(theme, "active"),
-        background: backgroundColor(theme, 300),
       },
     },
+    paneButtonContainer: {
+      background: tab.background,
+      border: {
+        ...tab.border,
+        right: false,
+      }
+    }
   }
 }

styles/src/styleTree/workspace.ts 🔗

@@ -37,11 +37,14 @@ export default function workspace(theme: Theme) {
       },
       cursor: "Arrow",
     },
-    sidebarResizeHandle: {
-      background: border(theme, "primary").color,
-      padding: {
-        left: 1,
-      },
+    sidebar: {
+      initialSize: 240,
+      border: {
+        color: border(theme, "primary").color,
+        width: 1,
+        left: true,
+        right: true,
+      }
     },
     paneDivider: {
       color: border(theme, "secondary").color,
@@ -156,5 +159,21 @@ export default function workspace(theme: Theme) {
       width: 400,
       margin: { right: 10, bottom: 10 },
     },
+    dock: {
+      initialSizeRight: 640,
+      initialSizeBottom: 480,
+      wash_color: withOpacity(theme.backgroundColor[500].base, 0.5),
+      panel: {
+        border: {
+          ...border(theme, "secondary"),
+          width: 1
+        },
+      },
+      maximized: {
+        margin: 24,
+        border: border(theme, "secondary", { "overlay": true }),
+        shadow: modalShadow(theme),
+      }
+    }
   };
 }