Merge branch 'main' into v0.119.x

Joseph T. Lyons created

Change summary

.github/actions/check_style/action.yml                                         |  30 
.zed/settings.json                                                             |   3 
Cargo.lock                                                                     |  91 
assets/settings/default.json                                                   |   2 
crates/activity_indicator/src/activity_indicator.rs                            |   2 
crates/assistant/src/assistant_panel.rs                                        |  44 
crates/auto_update/src/update_notification.rs                                  |   6 
crates/breadcrumbs/src/breadcrumbs.rs                                          |   4 
crates/call/src/call.rs                                                        |   2 
crates/call/src/room.rs                                                        | 288 
crates/channel/src/channel_chat.rs                                             |   4 
crates/client/src/telemetry.rs                                                 |  66 
crates/collab/src/db/queries/messages.rs                                       |   1 
crates/collab/src/tests/channel_guest_tests.rs                                 |  14 
crates/collab/src/tests/channel_tests.rs                                       |   2 
crates/collab/src/tests/integration_tests.rs                                   | 180 
crates/collab/src/tests/test_server.rs                                         |   1 
crates/collab_ui/src/channel_view.rs                                           |   4 
crates/collab_ui/src/chat_panel.rs                                             | 376 
crates/collab_ui/src/collab_panel.rs                                           |  66 
crates/collab_ui/src/collab_panel/channel_modal.rs                             |  14 
crates/collab_ui/src/collab_panel/contact_finder.rs                            |   6 
crates/collab_ui/src/collab_titlebar_item.rs                                   |  18 
crates/collab_ui/src/collab_ui.rs                                              |  45 
crates/collab_ui/src/notification_panel.rs                                     |  20 
crates/collab_ui/src/notifications/collab_notification.rs                      |   6 
crates/collab_ui/src/notifications/incoming_call_notification.rs               |   2 
crates/collab_ui/src/notifications/project_shared_notification.rs              |   2 
crates/collab_ui/src/notifications/stories/collab_notification.rs              |   2 
crates/command_palette/Cargo.toml                                              |   3 
crates/command_palette/src/command_palette.rs                                  |  33 
crates/copilot_ui/src/sign_in.rs                                               |  12 
crates/diagnostics/src/diagnostics.rs                                          |  20 
crates/diagnostics/src/items.rs                                                |  14 
crates/editor/src/editor.rs                                                    | 282 
crates/editor/src/editor_tests.rs                                              | 161 
crates/editor/src/element.rs                                                   | 322 
crates/editor/src/items.rs                                                     |   8 
crates/editor/src/scroll/autoscroll.rs                                         |   4 
crates/feedback/src/feedback_modal.rs                                          |  11 
crates/file_finder/src/file_finder.rs                                          |   4 
crates/go_to_line/src/go_to_line.rs                                            |   8 
crates/gpui/Cargo.toml                                                         |   2 
crates/gpui/build.rs                                                           |  24 
crates/gpui/src/app.rs                                                         |   7 
crates/gpui/src/app/entity_map.rs                                              |   8 
crates/gpui/src/color.rs                                                       |  10 
crates/gpui/src/elements/div.rs                                                |  63 
crates/gpui/src/elements/img.rs                                                |   2 
crates/gpui/src/elements/list.rs                                               |   4 
crates/gpui/src/elements/text.rs                                               |   4 
crates/gpui/src/geometry.rs                                                    |   6 
crates/gpui/src/key_dispatch.rs                                                | 124 
crates/gpui/src/platform.rs                                                    |   7 
crates/gpui/src/platform/mac/display.rs                                        |  38 
crates/gpui/src/platform/mac/metal_renderer.rs                                 | 232 
crates/gpui/src/platform/mac/open_type.rs                                      |   2 
crates/gpui/src/platform/mac/platform.rs                                       |  77 
crates/gpui/src/platform/mac/text_system.rs                                    |   3 
crates/gpui/src/platform/mac/window.rs                                         |  38 
crates/gpui/src/platform/test/platform.rs                                      |   4 
crates/gpui/src/platform/test/window.rs                                        |  12 
crates/gpui/src/scene.rs                                                       | 321 
crates/gpui/src/style.rs                                                       | 106 
crates/gpui/src/styled.rs                                                      |  11 
crates/gpui/src/taffy.rs                                                       |  42 
crates/gpui/src/text_system.rs                                                 |  25 
crates/gpui/src/text_system/line_layout.rs                                     |  51 
crates/gpui/src/view.rs                                                        | 145 
crates/gpui/src/window.rs                                                      | 630 
crates/gpui_macros/src/register_action.rs                                      |  13 
crates/language_selector/src/language_selector.rs                              |   2 
crates/language_tools/src/lsp_log.rs                                           |  19 
crates/language_tools/src/syntax_tree_view.rs                                  |   8 
crates/live_kit_client/LiveKitBridge/Sources/LiveKitBridge/LiveKitBridge.swift |  12 
crates/live_kit_client/src/prod.rs                                             |  14 
crates/live_kit_client/src/test.rs                                             | 209 
crates/outline/src/outline.rs                                                  |   2 
crates/picker/src/picker.rs                                                    |   8 
crates/project/src/project.rs                                                  |  14 
crates/project_panel/src/project_panel.rs                                      |  58 
crates/project_symbols/src/project_symbols.rs                                  |   4 
crates/quick_action_bar/src/quick_action_bar.rs                                |   2 
crates/recent_projects/src/recent_projects.rs                                  |   4 
crates/refineable/derive_refineable/src/derive_refineable.rs                   |  11 
crates/rich_text/src/rich_text.rs                                              |  60 
crates/rpc/build.rs                                                            |   1 
crates/search/src/buffer_search.rs                                             |  17 
crates/search/src/project_search.rs                                            |  46 
crates/semantic_index/src/semantic_index_tests.rs                              |   2 
crates/story/src/story.rs                                                      |   8 
crates/storybook/Cargo.toml                                                    |   1 
crates/storybook/src/stories/overflow_scroll.rs                                |   4 
crates/storybook/src/stories/text.rs                                           |   2 
crates/storybook/src/storybook.rs                                              |  17 
crates/terminal_view/src/terminal_element.rs                                   |   2 
crates/terminal_view/src/terminal_panel.rs                                     |  15 
crates/terminal_view/src/terminal_view.rs                                      |  11 
crates/theme/src/default_colors.rs                                             |  29 
crates/theme/src/one_themes.rs                                                 |   6 
crates/theme/src/settings.rs                                                   |  43 
crates/theme/src/theme.rs                                                      |  14 
crates/theme_selector/src/theme_selector.rs                                    |   4 
crates/ui/src/components/avatar.rs                                             |  10 
crates/ui/src/components/button/button.rs                                      |   4 
crates/ui/src/components/button/button_like.rs                                 |   9 
crates/ui/src/components/button/icon_button.rs                                 |  39 
crates/ui/src/components/checkbox.rs                                           |   2 
crates/ui/src/components/context_menu.rs                                       |   8 
crates/ui/src/components/keybinding.rs                                         |   6 
crates/ui/src/components/list/list.rs                                          |   4 
crates/ui/src/components/list/list_header.rs                                   |   8 
crates/ui/src/components/list/list_item.rs                                     |  12 
crates/ui/src/components/list/list_sub_header.rs                               |   4 
crates/ui/src/components/popover.rs                                            |   6 
crates/ui/src/components/popover_menu.rs                                       |   2 
crates/ui/src/components/right_click_menu.rs                                   |   9 
crates/ui/src/components/stack.rs                                              |   4 
crates/ui/src/components/stories/checkbox.rs                                   |   6 
crates/ui/src/components/stories/icon_button.rs                                |  50 
crates/ui/src/components/stories/list_item.rs                                  |   2 
crates/ui/src/components/stories/tab.rs                                        |  17 
crates/ui/src/components/stories/tab_bar.rs                                    |   2 
crates/ui/src/components/stories/toggle_button.rs                              |   4 
crates/ui/src/components/tab.rs                                                |  14 
crates/ui/src/components/tab_bar.rs                                            |   8 
crates/ui/src/components/tooltip.rs                                            |   6 
crates/ui/src/prelude.rs                                                       |   2 
crates/vcs_menu/src/lib.rs                                                     |  10 
crates/welcome/src/base_keymap_picker.rs                                       |   2 
crates/welcome/src/welcome.rs                                                  |  43 
crates/workspace/src/dock.rs                                                   |  24 
crates/workspace/src/item.rs                                                   |  32 
crates/workspace/src/modal_layer.rs                                            |   6 
crates/workspace/src/notifications.rs                                          |   6 
crates/workspace/src/pane.rs                                                   | 159 
crates/workspace/src/pane_group.rs                                             |  10 
crates/workspace/src/persistence.rs                                            |   2 
crates/workspace/src/shared_screen.rs                                          |   8 
crates/workspace/src/status_bar.rs                                             |   6 
crates/workspace/src/toolbar.rs                                                |  10 
crates/workspace/src/workspace.rs                                              |  53 
crates/zed/src/languages/python.rs                                             |   2 
crates/zed/src/main.rs                                                         |  11 
crates/zed/src/zed.rs                                                          |  30 
script/squawk                                                                  |  32 
script/zed-local                                                               |  33 
147 files changed, 3,301 insertions(+), 2,259 deletions(-)

Detailed changes

.github/actions/check_style/action.yml 🔗

@@ -2,16 +2,22 @@ name: "Check formatting"
 description: "Checks code formatting use cargo fmt"
 
 runs:
-    using: "composite"
-    steps:
-        - name: cargo fmt
-          shell: bash -euxo pipefail {0}
-          run: cargo fmt --all -- --check
+  using: "composite"
+  steps:
+    - name: cargo fmt
+      shell: bash -euxo pipefail {0}
+      run: cargo fmt --all -- --check
 
-        - name: cargo clippy
-          shell: bash -euxo pipefail {0}
-          # clippy.toml is not currently supporting specifying allowed lints
-          # so specify those here, and disable the rest until Zed's workspace
-          # will have more fixes & suppression for the standard lint set
-          run: |
-              cargo clippy --workspace --all-features --all-targets -- -A clippy::all -D clippy::dbg_macro -D clippy::todo
+    - name: cargo clippy
+      shell: bash -euxo pipefail {0}
+      # clippy.toml is not currently supporting specifying allowed lints
+      # so specify those here, and disable the rest until Zed's workspace
+      # will have more fixes & suppression for the standard lint set
+      run: |
+        cargo clippy --workspace --all-features --all-targets -- -A clippy::all -D clippy::dbg_macro -D clippy::todo
+
+    - name: Find modified migrations
+      shell: bash -euxo pipefail {0}
+      run: |
+        export SQUAWK_GITHUB_TOKEN=${{ github.token }}
+        . ./script/squawk

Cargo.lock 🔗

@@ -1607,6 +1607,7 @@ name = "command_palette"
 version = "0.1.0"
 dependencies = [
  "anyhow",
+ "client",
  "collections",
  "ctor",
  "editor",
@@ -1949,6 +1950,16 @@ dependencies = [
  "syn 2.0.37",
 ]
 
+[[package]]
+name = "ctrlc"
+version = "3.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b467862cc8610ca6fc9a1532d7777cee0804e678ab45410897b9396495994a0b"
+dependencies = [
+ "nix 0.27.1",
+ "windows-sys 0.52.0",
+]
+
 [[package]]
 name = "curl"
 version = "0.4.44"
@@ -2610,7 +2621,7 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
 [[package]]
 name = "font-kit"
 version = "0.11.0"
-source = "git+https://github.com/zed-industries/font-kit?rev=b2f77d56f450338aa4f7dd2f0197d8c9acb0cf18#b2f77d56f450338aa4f7dd2f0197d8c9acb0cf18"
+source = "git+https://github.com/zed-industries/font-kit?rev=d97147f#d97147ff11a9024b9707d9c9c7e3a0bdaba048ac"
 dependencies = [
  "bitflags 1.3.2",
  "byteorder",
@@ -4489,6 +4500,17 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "nix"
+version = "0.27.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
+dependencies = [
+ "bitflags 2.4.1",
+ "cfg-if 1.0.0",
+ "libc",
+]
+
 [[package]]
 name = "node_runtime"
 version = "0.1.0"
@@ -7452,6 +7474,7 @@ dependencies = [
  "chrono",
  "clap 4.4.4",
  "collab_ui",
+ "ctrlc",
  "dialoguer",
  "editor",
  "fuzzy",
@@ -9288,6 +9311,15 @@ dependencies = [
  "windows-targets 0.48.5",
 ]
 
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.0",
+]
+
 [[package]]
 name = "windows-targets"
 version = "0.42.2"
@@ -9318,6 +9350,21 @@ dependencies = [
  "windows_x86_64_msvc 0.48.5",
 ]
 
+[[package]]
+name = "windows-targets"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.0",
+ "windows_aarch64_msvc 0.52.0",
+ "windows_i686_gnu 0.52.0",
+ "windows_i686_msvc 0.52.0",
+ "windows_x86_64_gnu 0.52.0",
+ "windows_x86_64_gnullvm 0.52.0",
+ "windows_x86_64_msvc 0.52.0",
+]
+
 [[package]]
 name = "windows_aarch64_gnullvm"
 version = "0.42.2"
@@ -9330,6 +9377,12 @@ version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
 
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea"
+
 [[package]]
 name = "windows_aarch64_msvc"
 version = "0.42.2"
@@ -9342,6 +9395,12 @@ version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
 
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef"
+
 [[package]]
 name = "windows_i686_gnu"
 version = "0.42.2"
@@ -9354,6 +9413,12 @@ version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
 
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313"
+
 [[package]]
 name = "windows_i686_msvc"
 version = "0.42.2"
@@ -9366,6 +9431,12 @@ version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
 
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a"
+
 [[package]]
 name = "windows_x86_64_gnu"
 version = "0.42.2"
@@ -9378,6 +9449,12 @@ version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
 
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd"
+
 [[package]]
 name = "windows_x86_64_gnullvm"
 version = "0.42.2"
@@ -9390,6 +9467,12 @@ version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
 
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e"
+
 [[package]]
 name = "windows_x86_64_msvc"
 version = "0.42.2"
@@ -9402,6 +9485,12 @@ version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
 
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
+
 [[package]]
 name = "winnow"
 version = "0.5.15"

assets/settings/default.json 🔗

@@ -36,7 +36,7 @@
   //         },
   "buffer_line_height": "comfortable",
   // The name of a font to use for rendering text in the UI
-  "ui_font_family": "Zed Mono",
+  "ui_font_family": "Zed Sans",
   // The OpenType features to enable for text in the UI
   "ui_font_features": {
     // Disable ligatures:

crates/activity_indicator/src/activity_indicator.rs 🔗

@@ -295,7 +295,7 @@ impl Render for ActivityIndicator {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
         let content = self.content_to_render(cx);
 
-        let mut result = h_stack()
+        let mut result = h_flex()
             .id("activity-indicator")
             .on_action(cx.listener(Self::show_error_message))
             .on_action(cx.listener(Self::dismiss_error_message));

crates/assistant/src/assistant_panel.rs 🔗

@@ -40,7 +40,7 @@ use language::{language_settings::SoftWrap, Buffer, LanguageRegistry, ToOffset a
 use project::Project;
 use search::{buffer_search::DivRegistrar, BufferSearchBar};
 use semantic_index::{SemanticIndex, SemanticIndexStatus};
-use settings::{Settings, SettingsStore};
+use settings::Settings;
 use std::{
     cell::Cell,
     cmp,
@@ -165,7 +165,7 @@ impl AssistantPanel {
                     cx.on_focus_in(&focus_handle, Self::focus_in).detach();
                     cx.on_focus_out(&focus_handle, Self::focus_out).detach();
 
-                    let mut this = Self {
+                    Self {
                         workspace: workspace_handle,
                         active_editor_index: Default::default(),
                         prev_active_editor_index: Default::default(),
@@ -190,20 +190,7 @@ impl AssistantPanel {
                         _watch_saved_conversations,
                         semantic_index,
                         retrieve_context_in_next_inline_assist: false,
-                    };
-
-                    let mut old_dock_position = this.position(cx);
-                    this.subscriptions =
-                        vec![cx.observe_global::<SettingsStore>(move |this, cx| {
-                            let new_dock_position = this.position(cx);
-                            if new_dock_position != old_dock_position {
-                                old_dock_position = new_dock_position;
-                                cx.emit(PanelEvent::ChangePosition);
-                            }
-                            cx.notify();
-                        })];
-
-                    this
+                    }
                 })
             })
         })
@@ -1103,7 +1090,7 @@ fn build_api_key_editor(cx: &mut ViewContext<AssistantPanel>) -> View<Editor> {
 impl Render for AssistantPanel {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
         if let Some(api_key_editor) = self.api_key_editor.clone() {
-            v_stack()
+            v_flex()
                 .on_action(cx.listener(AssistantPanel::save_credentials))
                 .track_focus(&self.focus_handle)
                 .child(Label::new(
@@ -1128,26 +1115,26 @@ impl Render for AssistantPanel {
         } else {
             let header = TabBar::new("assistant_header")
                 .start_child(
-                    h_stack().gap_1().child(Self::render_hamburger_button(cx)), // .children(title),
+                    h_flex().gap_1().child(Self::render_hamburger_button(cx)), // .children(title),
                 )
                 .children(self.active_editor().map(|editor| {
-                    h_stack()
-                        .h(rems(Tab::HEIGHT_IN_REMS))
+                    h_flex()
+                        .h(rems(Tab::CONTAINER_HEIGHT_IN_REMS))
                         .flex_1()
                         .px_2()
                         .child(Label::new(editor.read(cx).title(cx)).into_element())
                 }))
                 .end_child(if self.focus_handle.contains_focused(cx) {
-                    h_stack()
+                    h_flex()
                         .gap_2()
-                        .child(h_stack().gap_1().children(self.render_editor_tools(cx)))
+                        .child(h_flex().gap_1().children(self.render_editor_tools(cx)))
                         .child(
                             ui::Divider::vertical()
                                 .inset()
                                 .color(ui::DividerColor::Border),
                         )
                         .child(
-                            h_stack()
+                            h_flex()
                                 .gap_1()
                                 .child(Self::render_plus_button(cx))
                                 .child(self.render_zoom_button(cx)),
@@ -1166,7 +1153,7 @@ impl Render for AssistantPanel {
             } else {
                 div()
             };
-            v_stack()
+            v_flex()
                 .key_context("AssistantPanel")
                 .size_full()
                 .on_action(cx.listener(|this, _: &workspace::NewFile, cx| {
@@ -2543,7 +2530,7 @@ impl Render for ConversationEditor {
                     .child(self.editor.clone()),
             )
             .child(
-                h_stack()
+                h_flex()
                     .absolute()
                     .gap_1()
                     .top_3()
@@ -2629,7 +2616,7 @@ impl EventEmitter<InlineAssistantEvent> for InlineAssistant {}
 impl Render for InlineAssistant {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Element {
         let measurements = self.measurements.get();
-        h_stack()
+        h_flex()
             .w_full()
             .py_2()
             .border_y_1()
@@ -2641,7 +2628,7 @@ impl Render for InlineAssistant {
             .on_action(cx.listener(Self::move_up))
             .on_action(cx.listener(Self::move_down))
             .child(
-                h_stack()
+                h_flex()
                     .justify_center()
                     .w(measurements.gutter_width)
                     .child(
@@ -2689,7 +2676,7 @@ impl Render for InlineAssistant {
                     }),
             )
             .child(
-                h_stack()
+                h_flex()
                     .w_full()
                     .ml(measurements.anchor_x - measurements.gutter_width)
                     .child(self.render_prompt_editor(cx)),
@@ -3133,6 +3120,7 @@ mod tests {
     use crate::MessageId;
     use ai::test::FakeCompletionProvider;
     use gpui::AppContext;
+    use settings::SettingsStore;
 
     #[gpui::test]
     fn test_inserting_and_removing_messages(cx: &mut AppContext) {

crates/auto_update/src/update_notification.rs 🔗

@@ -4,7 +4,7 @@ use gpui::{
 };
 use menu::Cancel;
 use util::channel::ReleaseChannel;
-use workspace::ui::{h_stack, v_stack, Icon, IconName, Label, StyledExt};
+use workspace::ui::{h_flex, v_flex, Icon, IconName, Label, StyledExt};
 
 pub struct UpdateNotification {
     version: SemanticVersion,
@@ -16,12 +16,12 @@ impl Render for UpdateNotification {
     fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
         let app_name = cx.global::<ReleaseChannel>().display_name();
 
-        v_stack()
+        v_flex()
             .on_action(cx.listener(UpdateNotification::dismiss))
             .elevation_3(cx)
             .p_4()
             .child(
-                h_stack()
+                h_flex()
                     .justify_between()
                     .child(Label::new(format!(
                         "Updated to {app_name} {}",

crates/breadcrumbs/src/breadcrumbs.rs 🔗

@@ -31,7 +31,7 @@ impl EventEmitter<ToolbarItemEvent> for Breadcrumbs {}
 
 impl Render for Breadcrumbs {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
-        let element = h_stack().text_ui();
+        let element = h_flex().text_ui();
         let Some(active_item) = self.active_item.as_ref() else {
             return element;
         };
@@ -51,7 +51,7 @@ impl Render for Breadcrumbs {
             Label::new("›").color(Color::Muted).into_any_element()
         });
 
-        let breadcrumbs_stack = h_stack().gap_1().children(breadcrumbs);
+        let breadcrumbs_stack = h_flex().gap_1().children(breadcrumbs);
         match active_item
             .downcast::<Editor>()
             .map(|editor| editor.downgrade())

crates/call/src/call.rs 🔗

@@ -442,6 +442,8 @@ impl ActiveCall {
                         .location
                         .as_ref()
                         .and_then(|location| location.upgrade());
+                    let channel_id = room.read(cx).channel_id();
+                    cx.emit(Event::RoomJoined { channel_id });
                     room.update(cx, |room, cx| room.set_location(location.as_ref(), cx))
                 }
             } else {

crates/call/src/room.rs 🔗

@@ -26,6 +26,9 @@ pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
 
 #[derive(Clone, Debug, PartialEq, Eq)]
 pub enum Event {
+    RoomJoined {
+        channel_id: Option<u64>,
+    },
     ParticipantLocationChanged {
         participant_id: proto::PeerId,
     },
@@ -49,7 +52,9 @@ pub enum Event {
     RemoteProjectInvitationDiscarded {
         project_id: u64,
     },
-    Left,
+    Left {
+        channel_id: Option<u64>,
+    },
 }
 
 pub struct Room {
@@ -150,17 +155,17 @@ impl Room {
             let connect = room.connect(&connection_info.server_url, &connection_info.token);
             cx.spawn(|this, mut cx| async move {
                 connect.await?;
-
-                let is_read_only = this
-                    .update(&mut cx, |room, _| room.read_only())
-                    .unwrap_or(true);
-
-                if !cx.update(|cx| Self::mute_on_join(cx))? && !is_read_only {
-                    this.update(&mut cx, |this, cx| this.share_microphone(cx))?
-                        .await?;
-                }
-
-                anyhow::Ok(())
+                this.update(&mut cx, |this, cx| {
+                    if !this.read_only() {
+                        if let Some(live_kit) = &this.live_kit {
+                            if !live_kit.muted_by_user && !live_kit.deafened {
+                                return this.share_microphone(cx);
+                            }
+                        }
+                    }
+                    Task::ready(Ok(()))
+                })?
+                .await
             })
             .detach_and_log_err(cx);
 
@@ -169,7 +174,7 @@ impl Room {
                 screen_track: LocalTrack::None,
                 microphone_track: LocalTrack::None,
                 next_publish_id: 0,
-                muted_by_user: false,
+                muted_by_user: Self::mute_on_join(cx),
                 deafened: false,
                 speaking: false,
                 _maintain_room,
@@ -357,7 +362,9 @@ impl Room {
 
     pub(crate) fn leave(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
         cx.notify();
-        cx.emit(Event::Left);
+        cx.emit(Event::Left {
+            channel_id: self.channel_id(),
+        });
         self.leave_internal(cx)
     }
 
@@ -598,6 +605,14 @@ impl Room {
             .map(|participant| participant.role)
     }
 
+    pub fn contains_guests(&self) -> bool {
+        self.local_participant.role == proto::ChannelRole::Guest
+            || self
+                .remote_participants
+                .values()
+                .any(|p| p.role == proto::ChannelRole::Guest)
+    }
+
     pub fn local_participant_is_admin(&self) -> bool {
         self.local_participant.role == proto::ChannelRole::Admin
     }
@@ -1032,6 +1047,15 @@ impl Room {
             }
 
             RoomUpdate::SubscribedToRemoteAudioTrack(track, publication) => {
+                if let Some(live_kit) = &self.live_kit {
+                    if live_kit.deafened {
+                        track.stop();
+                        cx.foreground_executor()
+                            .spawn(publication.set_enabled(false))
+                            .detach();
+                    }
+                }
+
                 let user_id = track.publisher_id().parse()?;
                 let track_id = track.sid().to_string();
                 let participant = self
@@ -1286,15 +1310,12 @@ impl Room {
         })
     }
 
-    pub fn is_muted(&self, cx: &AppContext) -> bool {
-        self.live_kit
-            .as_ref()
-            .and_then(|live_kit| match &live_kit.microphone_track {
-                LocalTrack::None => Some(Self::mute_on_join(cx)),
-                LocalTrack::Pending { muted, .. } => Some(*muted),
-                LocalTrack::Published { muted, .. } => Some(*muted),
-            })
-            .unwrap_or(false)
+    pub fn is_muted(&self) -> bool {
+        self.live_kit.as_ref().map_or(false, |live_kit| {
+            matches!(live_kit.microphone_track, LocalTrack::None)
+                || live_kit.muted_by_user
+                || live_kit.deafened
+        })
     }
 
     pub fn read_only(&self) -> bool {
@@ -1316,16 +1337,11 @@ impl Room {
     pub fn share_microphone(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
         if self.status.is_offline() {
             return Task::ready(Err(anyhow!("room is offline")));
-        } else if self.is_sharing_mic() {
-            return Task::ready(Err(anyhow!("microphone was already shared")));
         }
 
         let publish_id = if let Some(live_kit) = self.live_kit.as_mut() {
             let publish_id = post_inc(&mut live_kit.next_publish_id);
-            live_kit.microphone_track = LocalTrack::Pending {
-                publish_id,
-                muted: false,
-            };
+            live_kit.microphone_track = LocalTrack::Pending { publish_id };
             cx.notify();
             publish_id
         } else {
@@ -1354,14 +1370,13 @@ impl Room {
                         .as_mut()
                         .ok_or_else(|| anyhow!("live-kit was not initialized"))?;
 
-                    let (canceled, muted) = if let LocalTrack::Pending {
+                    let canceled = if let LocalTrack::Pending {
                         publish_id: cur_publish_id,
-                        muted,
                     } = &live_kit.microphone_track
                     {
-                        (*cur_publish_id != publish_id, *muted)
+                        *cur_publish_id != publish_id
                     } else {
-                        (true, false)
+                        true
                     };
 
                     match publication {
@@ -1369,14 +1384,13 @@ impl Room {
                             if canceled {
                                 live_kit.room.unpublish_track(publication);
                             } else {
-                                if muted {
+                                if live_kit.muted_by_user || live_kit.deafened {
                                     cx.background_executor()
-                                        .spawn(publication.set_mute(muted))
+                                        .spawn(publication.set_mute(true))
                                         .detach();
                                 }
                                 live_kit.microphone_track = LocalTrack::Published {
                                     track_publication: publication,
-                                    muted,
                                 };
                                 cx.notify();
                             }
@@ -1405,10 +1419,7 @@ impl Room {
 
         let (displays, publish_id) = if let Some(live_kit) = self.live_kit.as_mut() {
             let publish_id = post_inc(&mut live_kit.next_publish_id);
-            live_kit.screen_track = LocalTrack::Pending {
-                publish_id,
-                muted: false,
-            };
+            live_kit.screen_track = LocalTrack::Pending { publish_id };
             cx.notify();
             (live_kit.room.display_sources(), publish_id)
         } else {
@@ -1442,14 +1453,13 @@ impl Room {
                         .as_mut()
                         .ok_or_else(|| anyhow!("live-kit was not initialized"))?;
 
-                    let (canceled, muted) = if let LocalTrack::Pending {
+                    let canceled = if let LocalTrack::Pending {
                         publish_id: cur_publish_id,
-                        muted,
                     } = &live_kit.screen_track
                     {
-                        (*cur_publish_id != publish_id, *muted)
+                        *cur_publish_id != publish_id
                     } else {
-                        (true, false)
+                        true
                     };
 
                     match publication {
@@ -1457,14 +1467,8 @@ impl Room {
                             if canceled {
                                 live_kit.room.unpublish_track(publication);
                             } else {
-                                if muted {
-                                    cx.background_executor()
-                                        .spawn(publication.set_mute(muted))
-                                        .detach();
-                                }
                                 live_kit.screen_track = LocalTrack::Published {
                                     track_publication: publication,
-                                    muted,
                                 };
                                 cx.notify();
                             }
@@ -1487,61 +1491,51 @@ impl Room {
         })
     }
 
-    pub fn toggle_mute(&mut self, cx: &mut ModelContext<Self>) -> Result<Task<Result<()>>> {
-        let should_mute = !self.is_muted(cx);
+    pub fn toggle_mute(&mut self, cx: &mut ModelContext<Self>) {
         if let Some(live_kit) = self.live_kit.as_mut() {
-            if matches!(live_kit.microphone_track, LocalTrack::None) {
-                return Ok(self.share_microphone(cx));
+            // When unmuting, undeafen if the user was deafened before.
+            let was_deafened = live_kit.deafened;
+            if live_kit.muted_by_user
+                || live_kit.deafened
+                || matches!(live_kit.microphone_track, LocalTrack::None)
+            {
+                live_kit.muted_by_user = false;
+                live_kit.deafened = false;
+            } else {
+                live_kit.muted_by_user = true;
             }
+            let muted = live_kit.muted_by_user;
+            let should_undeafen = was_deafened && !live_kit.deafened;
 
-            let (ret_task, old_muted) = live_kit.set_mute(should_mute, cx)?;
-            live_kit.muted_by_user = should_mute;
+            if let Some(task) = self.set_mute(muted, cx) {
+                task.detach_and_log_err(cx);
+            }
 
-            if old_muted == true && live_kit.deafened == true {
-                if let Some(task) = self.toggle_deafen(cx).ok() {
-                    task.detach();
+            if should_undeafen {
+                if let Some(task) = self.set_deafened(false, cx) {
+                    task.detach_and_log_err(cx);
                 }
             }
-
-            Ok(ret_task)
-        } else {
-            Err(anyhow!("LiveKit not started"))
         }
     }
 
-    pub fn toggle_deafen(&mut self, cx: &mut ModelContext<Self>) -> Result<Task<Result<()>>> {
+    pub fn toggle_deafen(&mut self, cx: &mut ModelContext<Self>) {
         if let Some(live_kit) = self.live_kit.as_mut() {
-            (*live_kit).deafened = !live_kit.deafened;
-
-            let mut tasks = Vec::with_capacity(self.remote_participants.len());
-            // Context notification is sent within set_mute itself.
-            let mut mute_task = None;
-            // When deafening, mute user's mic as well.
-            // When undeafening, unmute user's mic unless it was manually muted prior to deafening.
-            if live_kit.deafened || !live_kit.muted_by_user {
-                mute_task = Some(live_kit.set_mute(live_kit.deafened, cx)?.0);
-            };
-            for participant in self.remote_participants.values() {
-                for track in live_kit
-                    .room
-                    .remote_audio_track_publications(&participant.user.id.to_string())
-                {
-                    let deafened = live_kit.deafened;
-                    tasks.push(cx.foreground_executor().spawn(track.set_enabled(!deafened)));
-                }
+            // When deafening, mute the microphone if it was not already muted.
+            // When un-deafening, unmute the microphone, unless it was explicitly muted.
+            let deafened = !live_kit.deafened;
+            live_kit.deafened = deafened;
+            let should_change_mute = !live_kit.muted_by_user;
+
+            if let Some(task) = self.set_deafened(deafened, cx) {
+                task.detach_and_log_err(cx);
             }
 
-            Ok(cx.foreground_executor().spawn(async move {
-                if let Some(mute_task) = mute_task {
-                    mute_task.await?;
-                }
-                for task in tasks {
-                    task.await?;
+            if should_change_mute {
+                if let Some(task) = self.set_mute(deafened, cx) {
+                    task.detach_and_log_err(cx);
                 }
-                Ok(())
-            }))
-        } else {
-            Err(anyhow!("LiveKit not started"))
+            }
         }
     }
 
@@ -1572,6 +1566,70 @@ impl Room {
         }
     }
 
+    fn set_deafened(
+        &mut self,
+        deafened: bool,
+        cx: &mut ModelContext<Self>,
+    ) -> Option<Task<Result<()>>> {
+        let live_kit = self.live_kit.as_mut()?;
+        cx.notify();
+
+        let mut track_updates = Vec::new();
+        for participant in self.remote_participants.values() {
+            for publication in live_kit
+                .room
+                .remote_audio_track_publications(&participant.user.id.to_string())
+            {
+                track_updates.push(publication.set_enabled(!deafened));
+            }
+
+            for track in participant.audio_tracks.values() {
+                if deafened {
+                    track.stop();
+                } else {
+                    track.start();
+                }
+            }
+        }
+
+        Some(cx.foreground_executor().spawn(async move {
+            for result in futures::future::join_all(track_updates).await {
+                result?;
+            }
+            Ok(())
+        }))
+    }
+
+    fn set_mute(
+        &mut self,
+        should_mute: bool,
+        cx: &mut ModelContext<Room>,
+    ) -> Option<Task<Result<()>>> {
+        let live_kit = self.live_kit.as_mut()?;
+        cx.notify();
+
+        if should_mute {
+            Audio::play_sound(Sound::Mute, cx);
+        } else {
+            Audio::play_sound(Sound::Unmute, cx);
+        }
+
+        match &mut live_kit.microphone_track {
+            LocalTrack::None => {
+                if should_mute {
+                    None
+                } else {
+                    Some(self.share_microphone(cx))
+                }
+            }
+            LocalTrack::Pending { .. } => None,
+            LocalTrack::Published { track_publication } => Some(
+                cx.foreground_executor()
+                    .spawn(track_publication.set_mute(should_mute)),
+            ),
+        }
+    }
+
     #[cfg(any(test, feature = "test-support"))]
     pub fn set_display_sources(&self, sources: Vec<live_kit_client::MacOSDisplay>) {
         self.live_kit
@@ -1596,50 +1654,6 @@ struct LiveKitRoom {
 }
 
 impl LiveKitRoom {
-    fn set_mute(
-        self: &mut LiveKitRoom,
-        should_mute: bool,
-        cx: &mut ModelContext<Room>,
-    ) -> Result<(Task<Result<()>>, bool)> {
-        if !should_mute {
-            // clear user muting state.
-            self.muted_by_user = false;
-        }
-
-        let (result, old_muted) = match &mut self.microphone_track {
-            LocalTrack::None => Err(anyhow!("microphone was not shared")),
-            LocalTrack::Pending { muted, .. } => {
-                let old_muted = *muted;
-                *muted = should_mute;
-                cx.notify();
-                Ok((Task::Ready(Some(Ok(()))), old_muted))
-            }
-            LocalTrack::Published {
-                track_publication,
-                muted,
-            } => {
-                let old_muted = *muted;
-                *muted = should_mute;
-                cx.notify();
-                Ok((
-                    cx.background_executor()
-                        .spawn(track_publication.set_mute(*muted)),
-                    old_muted,
-                ))
-            }
-        }?;
-
-        if old_muted != should_mute {
-            if should_mute {
-                Audio::play_sound(Sound::Mute, cx);
-            } else {
-                Audio::play_sound(Sound::Unmute, cx);
-            }
-        }
-
-        Ok((result, old_muted))
-    }
-
     fn stop_publishing(&mut self, cx: &mut ModelContext<Room>) {
         if let LocalTrack::Published {
             track_publication, ..
@@ -1663,11 +1677,9 @@ enum LocalTrack {
     None,
     Pending {
         publish_id: usize,
-        muted: bool,
     },
     Published {
         track_publication: LocalTrackPublication,
-        muted: bool,
     },
 }
 

crates/channel/src/channel_chat.rs 🔗

@@ -144,7 +144,7 @@ impl ChannelChat {
         message: MessageParams,
         cx: &mut ModelContext<Self>,
     ) -> Result<Task<Result<u64>>> {
-        if message.text.is_empty() {
+        if message.text.trim().is_empty() {
             Err(anyhow!("message body can't be empty"))?;
         }
 
@@ -174,6 +174,8 @@ impl ChannelChat {
         let user_store = self.user_store.clone();
         let rpc = self.rpc.clone();
         let outgoing_messages_lock = self.outgoing_messages_lock.clone();
+
+        // todo - handle messages that fail to send (e.g. >1024 chars)
         Ok(cx.spawn(move |this, mut cx| async move {
             let outgoing_message_guard = outgoing_messages_lock.lock().await;
             let request = rpc.request(proto::SendChannelMessage {

crates/client/src/telemetry.rs 🔗

@@ -14,6 +14,8 @@ use sysinfo::{
 };
 use tempfile::NamedTempFile;
 use util::http::HttpClient;
+#[cfg(not(debug_assertions))]
+use util::ResultExt;
 use util::{channel::ReleaseChannel, TryFutureExt};
 
 use self::event_coalescer::EventCoalescer;
@@ -114,7 +116,7 @@ pub enum Event {
         milliseconds_since_first_event: i64,
     },
     App {
-        operation: &'static str,
+        operation: String,
         milliseconds_since_first_event: i64,
     },
     Setting {
@@ -127,6 +129,11 @@ pub enum Event {
         environment: &'static str,
         milliseconds_since_first_event: i64,
     },
+    Action {
+        source: &'static str,
+        action: String,
+        milliseconds_since_first_event: i64,
+    },
 }
 
 #[cfg(debug_assertions)]
@@ -167,6 +174,20 @@ impl Telemetry {
             event_coalescer: EventCoalescer::new(),
         }));
 
+        #[cfg(not(debug_assertions))]
+        cx.background_executor()
+            .spawn({
+                let state = state.clone();
+                async move {
+                    if let Some(tempfile) =
+                        NamedTempFile::new_in(util::paths::CONFIG_DIR.as_path()).log_err()
+                    {
+                        state.lock().log_file = Some(tempfile);
+                    }
+                }
+            })
+            .detach();
+
         cx.observe_global::<SettingsStore>({
             let state = state.clone();
 
@@ -203,7 +224,7 @@ impl Telemetry {
     // TestAppContext ends up calling this function on shutdown and it panics when trying to find the TelemetrySettings
     #[cfg(not(any(test, feature = "test-support")))]
     fn shutdown_telemetry(self: &Arc<Self>) -> impl Future<Output = ()> {
-        self.report_app_event("close");
+        self.report_app_event("close".to_string());
         // TODO: close final edit period and make sure it's sent
         Task::ready(())
     }
@@ -369,7 +390,7 @@ impl Telemetry {
         self.report_event(event)
     }
 
-    pub fn report_app_event(self: &Arc<Self>, operation: &'static str) {
+    pub fn report_app_event(self: &Arc<Self>, operation: String) {
         let event = Event::App {
             operation,
             milliseconds_since_first_event: self.milliseconds_since_first_event(),
@@ -388,20 +409,6 @@ impl Telemetry {
         self.report_event(event)
     }
 
-    fn milliseconds_since_first_event(&self) -> i64 {
-        let mut state = self.state.lock();
-        match state.first_event_datetime {
-            Some(first_event_datetime) => {
-                let now: DateTime<Utc> = Utc::now();
-                now.timestamp_millis() - first_event_datetime.timestamp_millis()
-            }
-            None => {
-                state.first_event_datetime = Some(Utc::now());
-                0
-            }
-        }
-    }
-
     pub fn log_edit_event(self: &Arc<Self>, environment: &'static str) {
         let mut state = self.state.lock();
         let period_data = state.event_coalescer.log_event(environment);
@@ -418,6 +425,31 @@ impl Telemetry {
         }
     }
 
+    pub fn report_action_event(self: &Arc<Self>, source: &'static str, action: String) {
+        let event = Event::Action {
+            source,
+            action,
+            milliseconds_since_first_event: self.milliseconds_since_first_event(),
+        };
+
+        self.report_event(event)
+    }
+
+    fn milliseconds_since_first_event(&self) -> i64 {
+        let mut state = self.state.lock();
+
+        match state.first_event_datetime {
+            Some(first_event_datetime) => {
+                let now: DateTime<Utc> = Utc::now();
+                now.timestamp_millis() - first_event_datetime.timestamp_millis()
+            }
+            None => {
+                state.first_event_datetime = Some(Utc::now());
+                0
+            }
+        }
+    }
+
     fn report_event(self: &Arc<Self>, event: Event) {
         let mut state = self.state.lock();
 

crates/collab/src/db/queries/messages.rs 🔗

@@ -256,6 +256,7 @@ impl Database {
                     message_id = result.last_insert_id;
                     let mentioned_user_ids =
                         mentions.iter().map(|m| m.user_id).collect::<HashSet<_>>();
+
                     let mentions = mentions
                         .iter()
                         .filter_map(|mention| {

crates/collab/src/tests/channel_guest_tests.rs 🔗

@@ -57,7 +57,7 @@ async fn test_channel_guests(
         })
         .await
         .is_err());
-    assert!(room_b.read_with(cx_b, |room, _| !room.is_sharing_mic()));
+    assert!(room_b.read_with(cx_b, |room, _| room.is_muted()));
 }
 
 #[gpui::test]
@@ -104,6 +104,7 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
     });
     assert!(project_b.read_with(cx_b, |project, _| project.is_read_only()));
     assert!(editor_b.update(cx_b, |e, cx| e.read_only(cx)));
+    assert!(room_b.read_with(cx_b, |room, _| room.read_only()));
     assert!(room_b
         .update(cx_b, |room, cx| room.share_microphone(cx))
         .await
@@ -127,10 +128,13 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
     // project and buffers are now editable
     assert!(project_b.read_with(cx_b, |project, _| !project.is_read_only()));
     assert!(editor_b.update(cx_b, |editor, cx| !editor.read_only(cx)));
-    room_b
-        .update(cx_b, |room, cx| room.share_microphone(cx))
-        .await
-        .unwrap();
+
+    // B sees themselves as muted, and can unmute.
+    assert!(room_b.read_with(cx_b, |room, _| !room.read_only()));
+    room_b.read_with(cx_b, |room, _| assert!(room.is_muted()));
+    room_b.update(cx_b, |room, cx| room.toggle_mute(cx));
+    cx_a.run_until_parked();
+    room_b.read_with(cx_b, |room, _| assert!(!room.is_muted()));
 
     // B is demoted
     active_call_a

crates/collab/src/tests/channel_tests.rs 🔗

@@ -1418,8 +1418,6 @@ async fn test_channel_moving(
 ) {
     let mut server = TestServer::start(executor.clone()).await;
     let client_a = server.create_client(cx_a, "user_a").await;
-    // let client_b = server.create_client(cx_b, "user_b").await;
-    // let client_c = server.create_client(cx_c, "user_c").await;
 
     let channels = server
         .make_channel_tree(

crates/collab/src/tests/integration_tests.rs 🔗

@@ -1876,6 +1876,186 @@ fn active_call_events(cx: &mut TestAppContext) -> Rc<RefCell<Vec<room::Event>>>
     events
 }
 
+#[gpui::test]
+async fn test_mute_deafen(
+    executor: BackgroundExecutor,
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+    cx_c: &mut TestAppContext,
+) {
+    let mut server = TestServer::start(executor.clone()).await;
+    let client_a = server.create_client(cx_a, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+    let client_c = server.create_client(cx_c, "user_c").await;
+
+    server
+        .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
+        .await;
+
+    let active_call_a = cx_a.read(ActiveCall::global);
+    let active_call_b = cx_b.read(ActiveCall::global);
+    let active_call_c = cx_c.read(ActiveCall::global);
+
+    // User A calls user B, B answers.
+    active_call_a
+        .update(cx_a, |call, cx| {
+            call.invite(client_b.user_id().unwrap(), None, cx)
+        })
+        .await
+        .unwrap();
+    executor.run_until_parked();
+    active_call_b
+        .update(cx_b, |call, cx| call.accept_incoming(cx))
+        .await
+        .unwrap();
+    executor.run_until_parked();
+
+    let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
+    let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
+
+    room_a.read_with(cx_a, |room, _| assert!(!room.is_muted()));
+    room_b.read_with(cx_b, |room, _| assert!(!room.is_muted()));
+
+    // Users A and B are both muted.
+    assert_eq!(
+        participant_audio_state(&room_a, cx_a),
+        &[ParticipantAudioState {
+            user_id: client_b.user_id().unwrap(),
+            is_muted: false,
+            audio_tracks_playing: vec![true],
+        }]
+    );
+    assert_eq!(
+        participant_audio_state(&room_b, cx_b),
+        &[ParticipantAudioState {
+            user_id: client_a.user_id().unwrap(),
+            is_muted: false,
+            audio_tracks_playing: vec![true],
+        }]
+    );
+
+    // User A mutes
+    room_a.update(cx_a, |room, cx| room.toggle_mute(cx));
+    executor.run_until_parked();
+
+    // User A hears user B, but B doesn't hear A.
+    room_a.read_with(cx_a, |room, _| assert!(room.is_muted()));
+    room_b.read_with(cx_b, |room, _| assert!(!room.is_muted()));
+    assert_eq!(
+        participant_audio_state(&room_a, cx_a),
+        &[ParticipantAudioState {
+            user_id: client_b.user_id().unwrap(),
+            is_muted: false,
+            audio_tracks_playing: vec![true],
+        }]
+    );
+    assert_eq!(
+        participant_audio_state(&room_b, cx_b),
+        &[ParticipantAudioState {
+            user_id: client_a.user_id().unwrap(),
+            is_muted: true,
+            audio_tracks_playing: vec![true],
+        }]
+    );
+
+    // User A deafens
+    room_a.update(cx_a, |room, cx| room.toggle_deafen(cx));
+    executor.run_until_parked();
+
+    // User A does not hear user B.
+    room_a.read_with(cx_a, |room, _| assert!(room.is_muted()));
+    room_b.read_with(cx_b, |room, _| assert!(!room.is_muted()));
+    assert_eq!(
+        participant_audio_state(&room_a, cx_a),
+        &[ParticipantAudioState {
+            user_id: client_b.user_id().unwrap(),
+            is_muted: false,
+            audio_tracks_playing: vec![false],
+        }]
+    );
+    assert_eq!(
+        participant_audio_state(&room_b, cx_b),
+        &[ParticipantAudioState {
+            user_id: client_a.user_id().unwrap(),
+            is_muted: true,
+            audio_tracks_playing: vec![true],
+        }]
+    );
+
+    // User B calls user C, C joins.
+    active_call_b
+        .update(cx_b, |call, cx| {
+            call.invite(client_c.user_id().unwrap(), None, cx)
+        })
+        .await
+        .unwrap();
+    executor.run_until_parked();
+    active_call_c
+        .update(cx_c, |call, cx| call.accept_incoming(cx))
+        .await
+        .unwrap();
+    executor.run_until_parked();
+
+    // User A does not hear users B or C.
+    assert_eq!(
+        participant_audio_state(&room_a, cx_a),
+        &[
+            ParticipantAudioState {
+                user_id: client_b.user_id().unwrap(),
+                is_muted: false,
+                audio_tracks_playing: vec![false],
+            },
+            ParticipantAudioState {
+                user_id: client_c.user_id().unwrap(),
+                is_muted: false,
+                audio_tracks_playing: vec![false],
+            }
+        ]
+    );
+    assert_eq!(
+        participant_audio_state(&room_b, cx_b),
+        &[
+            ParticipantAudioState {
+                user_id: client_a.user_id().unwrap(),
+                is_muted: true,
+                audio_tracks_playing: vec![true],
+            },
+            ParticipantAudioState {
+                user_id: client_c.user_id().unwrap(),
+                is_muted: false,
+                audio_tracks_playing: vec![true],
+            }
+        ]
+    );
+
+    #[derive(PartialEq, Eq, Debug)]
+    struct ParticipantAudioState {
+        user_id: u64,
+        is_muted: bool,
+        audio_tracks_playing: Vec<bool>,
+    }
+
+    fn participant_audio_state(
+        room: &Model<Room>,
+        cx: &TestAppContext,
+    ) -> Vec<ParticipantAudioState> {
+        room.read_with(cx, |room, _| {
+            room.remote_participants()
+                .iter()
+                .map(|(user_id, participant)| ParticipantAudioState {
+                    user_id: *user_id,
+                    is_muted: participant.muted,
+                    audio_tracks_playing: participant
+                        .audio_tracks
+                        .values()
+                        .map(|track| track.is_playing())
+                        .collect(),
+                })
+                .collect::<Vec<_>>()
+        })
+    }
+}
+
 #[gpui::test(iterations = 10)]
 async fn test_room_location(
     executor: BackgroundExecutor,

crates/collab/src/tests/test_server.rs 🔗

@@ -248,7 +248,6 @@ impl TestServer {
             language::init(cx);
             editor::init(cx);
             workspace::init(app_state.clone(), cx);
-            audio::init((), cx);
             call::init(client.clone(), user_store.clone(), cx);
             channel::init(&client, user_store.clone(), cx);
             notifications::init(client.clone(), user_store, cx);

crates/collab_ui/src/channel_view.rs 🔗

@@ -266,6 +266,10 @@ impl Item for ChannelView {
             .into_any_element()
     }
 
+    fn telemetry_event_text(&self) -> Option<&'static str> {
+        None
+    }
+
     fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<View<Self>> {
         Some(cx.new_view(|cx| {
             Self::new(

crates/collab_ui/src/chat_panel.rs 🔗

@@ -1,15 +1,15 @@
-use crate::{channel_view::ChannelView, is_channels_feature_enabled, ChatPanelSettings};
+use crate::{collab_panel, is_channels_feature_enabled, ChatPanelSettings};
 use anyhow::Result;
-use call::ActiveCall;
+use call::{room, ActiveCall};
 use channel::{ChannelChat, ChannelChatEvent, ChannelMessageId, ChannelStore};
 use client::Client;
 use collections::HashMap;
 use db::kvp::KEY_VALUE_STORE;
 use editor::Editor;
 use gpui::{
-    actions, div, list, prelude::*, px, AnyElement, AppContext, AsyncWindowContext, ClickEvent,
-    ElementId, EventEmitter, FocusableView, ListOffset, ListScrollEvent, ListState, Model, Render,
-    Subscription, Task, View, ViewContext, VisualContext, WeakView,
+    actions, div, list, prelude::*, px, Action, AppContext, AsyncWindowContext, DismissEvent,
+    ElementId, EventEmitter, FocusHandle, FocusableView, FontWeight, ListOffset, ListScrollEvent,
+    ListState, Model, Render, Subscription, Task, View, ViewContext, VisualContext, WeakView,
 };
 use language::LanguageRegistry;
 use menu::Confirm;
@@ -17,10 +17,13 @@ use message_editor::MessageEditor;
 use project::Fs;
 use rich_text::RichText;
 use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsStore};
+use settings::Settings;
 use std::sync::Arc;
 use time::{OffsetDateTime, UtcOffset};
-use ui::{prelude::*, Avatar, Button, IconButton, IconName, Label, TabBar, Tooltip};
+use ui::{
+    popover_menu, prelude::*, Avatar, Button, ContextMenu, IconButton, IconName, KeyBinding, Label,
+    TabBar,
+};
 use util::{ResultExt, TryFutureExt};
 use workspace::{
     dock::{DockPosition, Panel, PanelEvent},
@@ -54,9 +57,10 @@ pub struct ChatPanel {
     active: bool,
     pending_serialization: Task<Option<()>>,
     subscriptions: Vec<gpui::Subscription>,
-    workspace: WeakView<Workspace>,
     is_scrolled_to_bottom: bool,
     markdown_data: HashMap<ChannelMessageId, RichText>,
+    focus_handle: FocusHandle,
+    open_context_menu: Option<(u64, Subscription)>,
 }
 
 #[derive(Serialize, Deserialize)]
@@ -64,13 +68,6 @@ struct SerializedChatPanel {
     width: Option<Pixels>,
 }
 
-#[derive(Debug)]
-pub enum Event {
-    DockPositionChanged,
-    Focus,
-    Dismissed,
-}
-
 actions!(chat_panel, [ToggleFocus]);
 
 impl ChatPanel {
@@ -89,8 +86,6 @@ impl ChatPanel {
             )
         });
 
-        let workspace_handle = workspace.weak_handle();
-
         cx.new_view(|cx: &mut ViewContext<Self>| {
             let view = cx.view().downgrade();
             let message_list =
@@ -108,7 +103,7 @@ impl ChatPanel {
                 if event.visible_range.start < MESSAGE_LOADING_THRESHOLD {
                     this.load_more_messages(cx);
                 }
-                this.is_scrolled_to_bottom = event.visible_range.end == event.count;
+                this.is_scrolled_to_bottom = !event.is_scrolled;
             }));
 
             let mut this = Self {
@@ -122,22 +117,37 @@ impl ChatPanel {
                 message_editor: input_editor,
                 local_timezone: cx.local_timezone(),
                 subscriptions: Vec::new(),
-                workspace: workspace_handle,
                 is_scrolled_to_bottom: true,
                 active: false,
                 width: None,
                 markdown_data: Default::default(),
+                focus_handle: cx.focus_handle(),
+                open_context_menu: None,
             };
 
-            let mut old_dock_position = this.position(cx);
-            this.subscriptions.push(cx.observe_global::<SettingsStore>(
-                move |this: &mut Self, cx| {
-                    let new_dock_position = this.position(cx);
-                    if new_dock_position != old_dock_position {
-                        old_dock_position = new_dock_position;
-                        cx.emit(Event::DockPositionChanged);
+            this.subscriptions.push(cx.subscribe(
+                &ActiveCall::global(cx),
+                move |this: &mut Self, call, event: &room::Event, cx| match event {
+                    room::Event::RoomJoined { channel_id } => {
+                        if let Some(channel_id) = channel_id {
+                            this.select_channel(*channel_id, None, cx)
+                                .detach_and_log_err(cx);
+
+                            if call
+                                .read(cx)
+                                .room()
+                                .is_some_and(|room| room.read(cx).contains_guests())
+                            {
+                                cx.emit(PanelEvent::Activate)
+                            }
+                        }
+                    }
+                    room::Event::Left { channel_id } => {
+                        if channel_id == &this.channel_id(cx) {
+                            cx.emit(PanelEvent::Close)
+                        }
                     }
-                    cx.notify();
+                    _ => {}
                 },
             ));
 
@@ -145,6 +155,12 @@ impl ChatPanel {
         })
     }
 
+    pub fn channel_id(&self, cx: &AppContext) -> Option<u64> {
+        self.active_chat
+            .as_ref()
+            .map(|(chat, _)| chat.read(cx).channel_id)
+    }
+
     pub fn is_scrolled_to_bottom(&self) -> bool {
         self.is_scrolled_to_bottom
     }
@@ -259,53 +275,9 @@ impl ChatPanel {
         }
     }
 
-    fn render_channel(&self, cx: &mut ViewContext<Self>) -> AnyElement {
-        v_stack()
-            .full()
-            .on_action(cx.listener(Self::send))
-            .child(
-                h_stack().z_index(1).child(
-                    TabBar::new("chat_header")
-                        .child(
-                            h_stack()
-                                .w_full()
-                                .h(rems(ui::Tab::HEIGHT_IN_REMS))
-                                .px_2()
-                                .child(Label::new(
-                                    self.active_chat
-                                        .as_ref()
-                                        .and_then(|c| {
-                                            Some(format!("#{}", c.0.read(cx).channel(cx)?.name))
-                                        })
-                                        .unwrap_or_default(),
-                                )),
-                        )
-                        .end_child(
-                            IconButton::new("notes", IconName::File)
-                                .on_click(cx.listener(Self::open_notes))
-                                .tooltip(|cx| Tooltip::text("Open notes", cx)),
-                        )
-                        .end_child(
-                            IconButton::new("call", IconName::AudioOn)
-                                .on_click(cx.listener(Self::join_call))
-                                .tooltip(|cx| Tooltip::text("Join call", cx)),
-                        ),
-                ),
-            )
-            .child(div().flex_grow().px_2().py_1().map(|this| {
-                if self.active_chat.is_some() {
-                    this.child(list(self.message_list.clone()).full())
-                } else {
-                    this
-                }
-            }))
-            .child(h_stack().p_2().child(self.message_editor.clone()))
-            .into_any()
-    }
-
     fn render_message(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> impl IntoElement {
         let active_chat = &self.active_chat.as_ref().unwrap().0;
-        let (message, is_continuation_from_previous, is_continuation_to_next, is_admin) =
+        let (message, is_continuation_from_previous, is_admin) =
             active_chat.update(cx, |active_chat, cx| {
                 let is_admin = self
                     .channel_store
@@ -314,13 +286,9 @@ impl ChatPanel {
 
                 let last_message = active_chat.message(ix.saturating_sub(1));
                 let this_message = active_chat.message(ix).clone();
-                let next_message =
-                    active_chat.message(ix.saturating_add(1).min(active_chat.message_count() - 1));
 
                 let is_continuation_from_previous = last_message.id != this_message.id
                     && last_message.sender.id == this_message.sender.id;
-                let is_continuation_to_next = this_message.id != next_message.id
-                    && this_message.sender.id == next_message.sender.id;
 
                 if let ChannelMessageId::Saved(id) = this_message.id {
                     if this_message
@@ -332,12 +300,7 @@ impl ChatPanel {
                     }
                 }
 
-                (
-                    this_message,
-                    is_continuation_from_previous,
-                    is_continuation_to_next,
-                    is_admin,
-                )
+                (this_message, is_continuation_from_previous, is_admin)
             });
 
         let _is_pending = message.is_pending();
@@ -360,50 +323,100 @@ impl ChatPanel {
             ChannelMessageId::Saved(id) => ("saved-message", id).into(),
             ChannelMessageId::Pending(id) => ("pending-message", id).into(),
         };
+        let this = cx.view().clone();
 
-        v_stack()
+        v_flex()
             .w_full()
-            .id(element_id)
             .relative()
             .overflow_hidden()
-            .group("")
             .when(!is_continuation_from_previous, |this| {
-                this.child(
-                    h_stack()
-                        .gap_2()
-                        .child(Avatar::new(message.sender.avatar_uri.clone()))
-                        .child(Label::new(message.sender.github_login.clone()))
+                this.pt_3().child(
+                    h_flex()
+                        .child(
+                            div().absolute().child(
+                                Avatar::new(message.sender.avatar_uri.clone())
+                                    .size(cx.rem_size() * 1.5),
+                            ),
+                        )
+                        .child(
+                            div()
+                                .pl(cx.rem_size() * 1.5 + px(6.0))
+                                .pr(px(8.0))
+                                .font_weight(FontWeight::BOLD)
+                                .child(Label::new(message.sender.github_login.clone())),
+                        )
                         .child(
                             Label::new(format_timestamp(
                                 message.timestamp,
                                 now,
                                 self.local_timezone,
                             ))
+                            .size(LabelSize::Small)
                             .color(Color::Muted),
                         ),
                 )
             })
-            .when(!is_continuation_to_next, |this|
-                // HACK: This should really be a margin, but margins seem to get collapsed.
-                this.pb_2())
-            .child(text.element("body".into(), cx))
+            .when(is_continuation_from_previous, |this| this.pt_1())
             .child(
-                div()
-                    .absolute()
-                    .top_1()
-                    .right_2()
-                    .w_8()
-                    .visible_on_hover("")
-                    .children(message_id_to_remove.map(|message_id| {
-                        IconButton::new(("remove", message_id), IconName::XCircle).on_click(
-                            cx.listener(move |this, _, cx| {
-                                this.remove_message(message_id, cx);
-                            }),
-                        )
-                    })),
+                v_flex()
+                    .w_full()
+                    .text_ui_sm()
+                    .id(element_id)
+                    .group("")
+                    .child(text.element("body".into(), cx))
+                    .child(
+                        div()
+                            .absolute()
+                            .z_index(1)
+                            .right_0()
+                            .w_6()
+                            .bg(cx.theme().colors().panel_background)
+                            .when(!self.has_open_menu(message_id_to_remove), |el| {
+                                el.visible_on_hover("")
+                            })
+                            .children(message_id_to_remove.map(|message_id| {
+                                popover_menu(("menu", message_id))
+                                    .trigger(IconButton::new(
+                                        ("trigger", message_id),
+                                        IconName::Ellipsis,
+                                    ))
+                                    .menu(move |cx| {
+                                        Some(Self::render_message_menu(&this, message_id, cx))
+                                    })
+                            })),
+                    ),
             )
     }
 
+    fn has_open_menu(&self, message_id: Option<u64>) -> bool {
+        match self.open_context_menu.as_ref() {
+            Some((id, _)) => Some(*id) == message_id,
+            None => false,
+        }
+    }
+
+    fn render_message_menu(
+        this: &View<Self>,
+        message_id: u64,
+        cx: &mut WindowContext,
+    ) -> View<ContextMenu> {
+        let menu = {
+            let this = this.clone();
+            ContextMenu::build(cx, move |menu, _| {
+                menu.entry("Delete message", None, move |cx| {
+                    this.update(cx, |this, cx| this.remove_message(message_id, cx))
+                })
+            })
+        };
+        this.update(cx, |this, cx| {
+            let subscription = cx.subscribe(&menu, |this: &mut Self, _, _: &DismissEvent, _| {
+                this.open_context_menu = None;
+            });
+            this.open_context_menu = Some((message_id, subscription));
+        });
+        menu
+    }
+
     fn render_markdown_with_mentions(
         language_registry: &Arc<LanguageRegistry>,
         current_user_id: u64,
@@ -421,44 +434,6 @@ impl ChatPanel {
         rich_text::render_markdown(message.body.clone(), &mentions, language_registry, None)
     }
 
-    fn render_sign_in_prompt(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
-        v_stack()
-            .gap_2()
-            .p_4()
-            .child(
-                Button::new("sign-in", "Sign in")
-                    .style(ButtonStyle::Filled)
-                    .icon_color(Color::Muted)
-                    .icon(IconName::Github)
-                    .icon_position(IconPosition::Start)
-                    .full_width()
-                    .on_click(cx.listener(move |this, _, cx| {
-                        let client = this.client.clone();
-                        cx.spawn(|this, mut cx| async move {
-                            if client
-                                .authenticate_and_connect(true, &cx)
-                                .log_err()
-                                .await
-                                .is_some()
-                            {
-                                this.update(&mut cx, |_, cx| {
-                                    cx.focus_self();
-                                })
-                                .ok();
-                            }
-                        })
-                        .detach();
-                    })),
-            )
-            .child(
-                div().flex().w_full().items_center().child(
-                    Label::new("Sign in to chat.")
-                        .color(Color::Muted)
-                        .size(LabelSize::Small),
-                ),
-            )
-    }
-
     fn send(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
         if let Some((chat, _)) = self.active_chat.as_ref() {
             let message = self
@@ -535,50 +510,93 @@ impl ChatPanel {
             Ok(())
         })
     }
-
-    fn open_notes(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
-        if let Some((chat, _)) = &self.active_chat {
-            let channel_id = chat.read(cx).channel_id;
-            if let Some(workspace) = self.workspace.upgrade() {
-                ChannelView::open(channel_id, workspace, cx).detach();
-            }
-        }
-    }
-
-    fn join_call(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
-        if let Some((chat, _)) = &self.active_chat {
-            let channel_id = chat.read(cx).channel_id;
-            ActiveCall::global(cx)
-                .update(cx, |call, cx| call.join_channel(channel_id, cx))
-                .detach_and_log_err(cx);
-        }
-    }
 }
 
-impl EventEmitter<Event> for ChatPanel {}
-
 impl Render for ChatPanel {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
-        v_stack()
-            .size_full()
-            .map(|this| match (self.client.user_id(), self.active_chat()) {
-                (Some(_), Some(_)) => this.child(self.render_channel(cx)),
-                (Some(_), None) => this.child(
-                    div().p_4().child(
-                        Label::new("Select a channel to chat in.")
-                            .size(LabelSize::Small)
-                            .color(Color::Muted),
+        v_flex()
+            .track_focus(&self.focus_handle)
+            .full()
+            .on_action(cx.listener(Self::send))
+            .child(
+                h_flex().z_index(1).child(
+                    TabBar::new("chat_header").child(
+                        h_flex()
+                            .w_full()
+                            .h(rems(ui::Tab::CONTAINER_HEIGHT_IN_REMS))
+                            .px_2()
+                            .child(Label::new(
+                                self.active_chat
+                                    .as_ref()
+                                    .and_then(|c| {
+                                        Some(format!("#{}", c.0.read(cx).channel(cx)?.name))
+                                    })
+                                    .unwrap_or("Chat".to_string()),
+                            )),
                     ),
                 ),
-                (None, _) => this.child(self.render_sign_in_prompt(cx)),
-            })
-            .min_w(px(150.))
+            )
+            .child(div().flex_grow().px_2().pt_1().map(|this| {
+                if self.active_chat.is_some() {
+                    this.child(list(self.message_list.clone()).full())
+                } else {
+                    this.child(
+                        div()
+                            .p_4()
+                            .child(
+                                Label::new("Select a channel to chat in.")
+                                    .size(LabelSize::Small)
+                                    .color(Color::Muted),
+                            )
+                            .child(
+                                div().pt_1().w_full().items_center().child(
+                                    Button::new("toggle-collab", "Open")
+                                        .full_width()
+                                        .key_binding(KeyBinding::for_action(
+                                            &collab_panel::ToggleFocus,
+                                            cx,
+                                        ))
+                                        .on_click(|_, cx| {
+                                            cx.dispatch_action(
+                                                collab_panel::ToggleFocus.boxed_clone(),
+                                            )
+                                        }),
+                                ),
+                            ),
+                    )
+                }
+            }))
+            .child(
+                h_flex()
+                    .when(!self.is_scrolled_to_bottom, |el| {
+                        el.border_t_1().border_color(cx.theme().colors().border)
+                    })
+                    .p_2()
+                    .map(|el| {
+                        if self.active_chat.is_some() {
+                            el.child(self.message_editor.clone())
+                        } else {
+                            el.child(
+                                div()
+                                    .rounded_md()
+                                    .h_7()
+                                    .w_full()
+                                    .bg(cx.theme().colors().editor_background),
+                            )
+                        }
+                    }),
+            )
+            .into_any()
     }
 }
 
 impl FocusableView for ChatPanel {
     fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
-        self.message_editor.read(cx).focus_handle(cx)
+        if self.active_chat.is_some() {
+            self.message_editor.read(cx).focus_handle(cx)
+        } else {
+            self.focus_handle.clone()
+        }
     }
 }
 
@@ -613,7 +631,7 @@ impl Panel for ChatPanel {
         if active {
             self.acknowledge_last_message(cx);
             if !is_channels_feature_enabled(cx) {
-                cx.emit(Event::Dismissed);
+                cx.emit(PanelEvent::Close);
             }
         }
     }

crates/collab_ui/src/collab_panel.rs 🔗

@@ -26,7 +26,7 @@ use menu::{Cancel, Confirm, SelectNext, SelectPrev};
 use project::{Fs, Project};
 use rpc::proto::{self, PeerId};
 use serde_derive::{Deserialize, Serialize};
-use settings::{Settings, SettingsStore};
+use settings::Settings;
 use smallvec::SmallVec;
 use std::{mem, sync::Arc};
 use theme::{ActiveTheme, ThemeSettings};
@@ -254,19 +254,6 @@ impl CollabPanel {
 
             this.update_entries(false, cx);
 
-            // Update the dock position when the setting changes.
-            let mut old_dock_position = this.position(cx);
-            this.subscriptions.push(cx.observe_global::<SettingsStore>(
-                move |this: &mut Self, cx| {
-                    let new_dock_position = this.position(cx);
-                    if new_dock_position != old_dock_position {
-                        old_dock_position = new_dock_position;
-                        cx.emit(PanelEvent::ChangePosition);
-                    }
-                    cx.notify();
-                },
-            ));
-
             let active_call = ActiveCall::global(cx);
             this.subscriptions
                 .push(cx.observe(&this.user_store, |this, _, cx| {
@@ -900,7 +887,7 @@ impl CollabPanel {
                     .ok();
             }))
             .start_slot(
-                h_stack()
+                h_flex()
                     .gap_1()
                     .child(render_tree_branch(is_last, false, cx))
                     .child(IconButton::new(0, IconName::Folder)),
@@ -921,7 +908,7 @@ impl CollabPanel {
         ListItem::new(("screen", id))
             .selected(is_selected)
             .start_slot(
-                h_stack()
+                h_flex()
                     .gap_1()
                     .child(render_tree_branch(is_last, false, cx))
                     .child(IconButton::new(0, IconName::Screen)),
@@ -962,7 +949,7 @@ impl CollabPanel {
                 this.open_channel_notes(channel_id, cx);
             }))
             .start_slot(
-                h_stack()
+                h_flex()
                     .gap_1()
                     .child(render_tree_branch(false, true, cx))
                     .child(IconButton::new(0, IconName::File)),
@@ -983,7 +970,7 @@ impl CollabPanel {
                 this.join_channel_chat(channel_id, cx);
             }))
             .start_slot(
-                h_stack()
+                h_flex()
                     .gap_1()
                     .child(render_tree_branch(false, false, cx))
                     .child(IconButton::new(0, IconName::MessageBubbles)),
@@ -1426,14 +1413,6 @@ impl CollabPanel {
         self.toggle_channel_collapsed(id, cx)
     }
 
-    //     fn toggle_channel_collapsed_action(
-    //         &mut self,
-    //         action: &ToggleCollapse,
-    //         cx: &mut ViewContext<Self>,
-    //     ) {
-    //         self.toggle_channel_collapsed(action.location, cx);
-    //     }
-
     fn toggle_channel_collapsed<'a>(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
         match self.collapsed_channels.binary_search(&channel_id) {
             Ok(ix) => {
@@ -1747,12 +1726,12 @@ impl CollabPanel {
     fn render_signed_out(&mut self, cx: &mut ViewContext<Self>) -> Div {
         let collab_blurb = "Work with your team in realtime with collaborative editing, voice, shared notes and more.";
 
-        v_stack()
+        v_flex()
             .gap_6()
             .p_4()
             .child(Label::new(collab_blurb))
             .child(
-                v_stack()
+                v_flex()
                     .gap_2()
                     .child(
                         Button::new("sign_in", "Sign in")
@@ -1853,14 +1832,14 @@ impl CollabPanel {
     }
 
     fn render_signed_in(&mut self, cx: &mut ViewContext<Self>) -> Div {
-        v_stack()
+        v_flex()
             .size_full()
             .child(list(self.list_state.clone()).full())
             .child(
-                v_stack()
+                v_flex()
                     .child(div().mx_2().border_primary(cx).border_t())
                     .child(
-                        v_stack()
+                        v_flex()
                             .p_2()
                             .child(self.render_filter_input(&self.filter_editor, cx)),
                     ),
@@ -1910,7 +1889,6 @@ impl CollabPanel {
         let mut channel_link = None;
         let mut channel_tooltip_text = None;
         let mut channel_icon = None;
-        // let mut is_dragged_over = false;
 
         let text = match section {
             Section::ActiveCall => {
@@ -1983,7 +1961,7 @@ impl CollabPanel {
             | Section::Offline => true,
         };
 
-        h_stack()
+        h_flex()
             .w_full()
             .group("section-header")
             .child(
@@ -2029,7 +2007,7 @@ impl CollabPanel {
                 .selected(is_selected)
                 .on_click(cx.listener(move |this, _, cx| this.call(user_id, cx)))
                 .child(
-                    h_stack()
+                    h_flex()
                         .w_full()
                         .justify_between()
                         .child(Label::new(github_login.clone()))
@@ -2052,7 +2030,7 @@ impl CollabPanel {
                         }),
                 )
                 .start_slot(
-                    // todo!() handle contacts with no avatar
+                    // todo handle contacts with no avatar
                     Avatar::new(contact.user.avatar_uri.clone())
                         .availability_indicator(if online { Some(!busy) } else { None }),
                 )
@@ -2127,11 +2105,11 @@ impl CollabPanel {
             .indent_step_size(px(20.))
             .selected(is_selected)
             .child(
-                h_stack()
+                h_flex()
                     .w_full()
                     .justify_between()
                     .child(Label::new(github_login.clone()))
-                    .child(h_stack().children(controls)),
+                    .child(h_flex().children(controls)),
             )
             .start_slot(Avatar::new(user.avatar_uri.clone()))
     }
@@ -2171,11 +2149,11 @@ impl CollabPanel {
         ListItem::new(("channel-invite", channel.id as usize))
             .selected(is_selected)
             .child(
-                h_stack()
+                h_flex()
                     .w_full()
                     .justify_between()
                     .child(Label::new(channel.name.clone()))
-                    .child(h_stack().children(controls)),
+                    .child(h_flex().children(controls)),
             )
             .start_slot(
                 Icon::new(IconName::Hash)
@@ -2311,21 +2289,21 @@ impl CollabPanel {
                         .color(Color::Muted),
                     )
                     .child(
-                        h_stack()
+                        h_flex()
                             .id(channel_id as usize)
                             .child(Label::new(channel.name.clone()))
                             .children(face_pile.map(|face_pile| face_pile.render(cx))),
                     ),
             )
             .child(
-                h_stack()
+                h_flex()
                     .absolute()
                     .right(rems(0.))
                     .h_full()
                     // HACK: Without this the channel name clips on top of the icons, but I'm not sure why.
                     .z_index(10)
                     .child(
-                        h_stack()
+                        h_flex()
                             .h_full()
                             .gap_1()
                             .px_1()
@@ -2432,7 +2410,7 @@ fn render_tree_branch(is_last: bool, overdraw: bool, cx: &mut WindowContext) ->
 
 impl Render for CollabPanel {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
-        v_stack()
+        v_flex()
             .key_context("CollabPanel")
             .on_action(cx.listener(CollabPanel::cancel))
             .on_action(cx.listener(CollabPanel::select_next))
@@ -2625,7 +2603,7 @@ struct DraggedChannelView {
 impl Render for DraggedChannelView {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Element {
         let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
-        h_stack()
+        h_flex()
             .font(ui_font)
             .bg(cx.theme().colors().background)
             .w(self.width)

crates/collab_ui/src/collab_panel/channel_modal.rs 🔗

@@ -152,19 +152,19 @@ impl Render for ChannelModal {
         let visibility = channel.visibility;
         let mode = self.picker.read(cx).delegate.mode;
 
-        v_stack()
+        v_flex()
             .key_context("ChannelModal")
             .on_action(cx.listener(Self::toggle_mode))
             .on_action(cx.listener(Self::dismiss))
             .elevation_3(cx)
             .w(rems(34.))
             .child(
-                v_stack()
+                v_flex()
                     .px_2()
                     .py_1()
                     .gap_2()
                     .child(
-                        h_stack()
+                        h_flex()
                             .w_px()
                             .flex_1()
                             .gap_1()
@@ -172,13 +172,13 @@ impl Render for ChannelModal {
                             .child(Label::new(channel_name)),
                     )
                     .child(
-                        h_stack()
+                        h_flex()
                             .w_full()
                             .h(rems(22. / 16.))
                             .justify_between()
                             .line_height(rems(1.25))
                             .child(
-                                h_stack()
+                                h_flex()
                                     .gap_2()
                                     .child(
                                         Checkbox::new(
@@ -212,7 +212,7 @@ impl Render for ChannelModal {
                             ),
                     )
                     .child(
-                        h_stack()
+                        h_flex()
                             .child(
                                 div()
                                     .id("manage-members")
@@ -391,7 +391,7 @@ impl PickerDelegate for ChannelModalDelegate {
                 .selected(selected)
                 .start_slot(Avatar::new(user.avatar_uri.clone()))
                 .child(Label::new(user.github_login.clone()))
-                .end_slot(h_stack().gap_2().map(|slot| {
+                .end_slot(h_flex().gap_2().map(|slot| {
                     match self.mode {
                         Mode::ManageMembers => slot
                             .children(

crates/collab_ui/src/collab_panel/contact_finder.rs 🔗

@@ -36,17 +36,17 @@ impl ContactFinder {
 
 impl Render for ContactFinder {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
-        v_stack()
+        v_flex()
             .elevation_3(cx)
             .child(
-                v_stack()
+                v_flex()
                     .px_2()
                     .py_1()
                     .bg(cx.theme().colors().element_background)
                     // HACK: Prevent the background color from overflowing the parent container.
                     .rounded_t(px(8.))
                     .child(Label::new("Contacts"))
-                    .child(h_stack().child(Label::new("Invite new contacts"))),
+                    .child(h_flex().child(Label::new("Invite new contacts"))),
             )
             .child(self.picker.clone())
             .w(rems(34.))

crates/collab_ui/src/collab_titlebar_item.rs 🔗

@@ -14,7 +14,7 @@ use rpc::proto;
 use std::sync::Arc;
 use theme::{ActiveTheme, PlayerColors};
 use ui::{
-    h_stack, popover_menu, prelude::*, Avatar, Button, ButtonLike, ButtonStyle, ContextMenu, Icon,
+    h_flex, popover_menu, prelude::*, Avatar, Button, ButtonLike, ButtonStyle, ContextMenu, Icon,
     IconButton, IconName, TintColor, Tooltip,
 };
 use util::ResultExt;
@@ -58,7 +58,7 @@ impl Render for CollabTitlebarItem {
         let client = self.client.clone();
         let project_id = self.project.read(cx).remote_id();
 
-        h_stack()
+        h_flex()
             .id("titlebar")
             .justify_between()
             .w_full()
@@ -83,7 +83,7 @@ impl Render for CollabTitlebarItem {
             })
             // left side
             .child(
-                h_stack()
+                h_flex()
                     .gap_1()
                     .children(self.render_project_host(cx))
                     .child(self.render_project_name(cx))
@@ -102,7 +102,7 @@ impl Render for CollabTitlebarItem {
                                 peer_id,
                                 true,
                                 room.is_speaking(),
-                                room.is_muted(cx),
+                                room.is_muted(),
                                 &room,
                                 project_id,
                                 &current_user,
@@ -128,7 +128,7 @@ impl Render for CollabTitlebarItem {
                                     )?;
 
                                     Some(
-                                        v_stack()
+                                        v_flex()
                                             .id(("collaborator", collaborator.user.id))
                                             .child(face_pile)
                                             .child(render_color_ribbon(
@@ -160,7 +160,7 @@ impl Render for CollabTitlebarItem {
             )
             // right side
             .child(
-                h_stack()
+                h_flex()
                     .gap_1()
                     .pr_1()
                     .when_some(room, |this, room| {
@@ -168,7 +168,7 @@ impl Render for CollabTitlebarItem {
                         let project = self.project.read(cx);
                         let is_local = project.is_local();
                         let is_shared = is_local && project.is_shared();
-                        let is_muted = room.is_muted(cx);
+                        let is_muted = room.is_muted();
                         let is_deafened = room.is_deafened().unwrap_or(false);
                         let is_screen_sharing = room.is_screen_sharing();
                         let read_only = room.read_only();
@@ -634,7 +634,7 @@ impl CollabTitlebarItem {
                 .trigger(
                     ButtonLike::new("user-menu")
                         .child(
-                            h_stack()
+                            h_flex()
                                 .gap_0p5()
                                 .child(Avatar::new(user.avatar_uri.clone()))
                                 .child(Icon::new(IconName::ChevronDown).color(Color::Muted)),
@@ -657,7 +657,7 @@ impl CollabTitlebarItem {
                 .trigger(
                     ButtonLike::new("user-menu")
                         .child(
-                            h_stack()
+                            h_flex()
                                 .gap_0p5()
                                 .child(Icon::new(IconName::ChevronDown).color(Color::Muted)),
                         )

crates/collab_ui/src/collab_ui.rs 🔗

@@ -9,7 +9,7 @@ mod panel_settings;
 
 use std::{rc::Rc, sync::Arc};
 
-use call::{report_call_event_for_room, ActiveCall, Room};
+use call::{report_call_event_for_room, ActiveCall};
 pub use collab_panel::CollabPanel;
 pub use collab_titlebar_item::CollabTitlebarItem;
 use feature_flags::{ChannelsAlpha, FeatureFlagAppExt};
@@ -21,7 +21,6 @@ pub use panel_settings::{
     ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings,
 };
 use settings::Settings;
-use util::ResultExt;
 use workspace::AppState;
 
 actions!(
@@ -41,10 +40,6 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
     chat_panel::init(cx);
     notification_panel::init(cx);
     notifications::init(&app_state, cx);
-
-    // cx.add_global_action(toggle_screen_sharing);
-    // cx.add_global_action(toggle_mute);
-    // cx.add_global_action(toggle_deafen);
 }
 
 pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
@@ -79,7 +74,7 @@ pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
     if let Some(room) = call.room().cloned() {
         let client = call.client();
         room.update(cx, |room, cx| {
-            let operation = if room.is_muted(cx) {
+            let operation = if room.is_muted() {
                 "enable microphone"
             } else {
                 "disable microphone"
@@ -87,17 +82,13 @@ pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
             report_call_event_for_room(operation, room.id(), room.channel_id(), &client);
 
             room.toggle_mute(cx)
-        })
-        .map(|task| task.detach_and_log_err(cx))
-        .log_err();
+        });
     }
 }
 
 pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) {
     if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
-        room.update(cx, Room::toggle_deafen)
-            .map(|task| task.detach_and_log_err(cx))
-            .log_err();
+        room.update(cx, |room, cx| room.toggle_deafen(cx));
     }
 }
 
@@ -131,34 +122,6 @@ fn notification_window_options(
     }
 }
 
-// fn render_avatar<T: 'static>(
-//     avatar: Option<Arc<ImageData>>,
-//     avatar_style: &AvatarStyle,
-//     container: ContainerStyle,
-// ) -> AnyElement<T> {
-//     avatar
-//         .map(|avatar| {
-//             Image::from_data(avatar)
-//                 .with_style(avatar_style.image)
-//                 .aligned()
-//                 .contained()
-//                 .with_corner_radius(avatar_style.outer_corner_radius)
-//                 .constrained()
-//                 .with_width(avatar_style.outer_width)
-//                 .with_height(avatar_style.outer_width)
-//                 .into_any()
-//         })
-//         .unwrap_or_else(|| {
-//             Empty::new()
-//                 .constrained()
-//                 .with_width(avatar_style.outer_width)
-//                 .into_any()
-//         })
-//         .contained()
-//         .with_style(container)
-//         .into_any()
-// }
-
 fn is_channels_feature_enabled(cx: &gpui::WindowContext<'_>) -> bool {
     cx.is_staff() || cx.has_flag::<ChannelsAlpha>()
 }

crates/collab_ui/src/notification_panel.rs 🔗

@@ -19,7 +19,7 @@ use serde::{Deserialize, Serialize};
 use settings::{Settings, SettingsStore};
 use std::{sync::Arc, time::Duration};
 use time::{OffsetDateTime, UtcOffset};
-use ui::{h_stack, prelude::*, v_stack, Avatar, Button, Icon, IconButton, IconName, Label};
+use ui::{h_flex, prelude::*, v_flex, Avatar, Button, Icon, IconButton, IconName, Label};
 use util::{ResultExt, TryFutureExt};
 use workspace::{
     dock::{DockPosition, Panel, PanelEvent},
@@ -251,13 +251,13 @@ impl NotificationPanel {
                         .rounded_full()
                 }))
                 .child(
-                    v_stack()
+                    v_flex()
                         .gap_1()
                         .size_full()
                         .overflow_hidden()
                         .child(Label::new(text.clone()))
                         .child(
-                            h_stack()
+                            h_flex()
                                 .child(
                                     Label::new(format_timestamp(
                                         timestamp,
@@ -276,7 +276,7 @@ impl NotificationPanel {
                                     )))
                                 } else if needs_response {
                                     Some(
-                                        h_stack()
+                                        h_flex()
                                             .flex_grow()
                                             .justify_end()
                                             .child(Button::new("decline", "Decline").on_click({
@@ -541,15 +541,15 @@ impl NotificationPanel {
 
 impl Render for NotificationPanel {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
-        v_stack()
+        v_flex()
             .size_full()
             .child(
-                h_stack()
+                h_flex()
                     .justify_between()
                     .px_2()
                     .py_1()
                     // Match the height of the tab bar so they line up.
-                    .h(rems(ui::Tab::HEIGHT_IN_REMS))
+                    .h(rems(ui::Tab::CONTAINER_HEIGHT_IN_REMS))
                     .border_b_1()
                     .border_color(cx.theme().colors().border)
                     .child(Label::new("Notifications"))
@@ -558,7 +558,7 @@ impl Render for NotificationPanel {
             .map(|this| {
                 if self.client.user_id().is_none() {
                     this.child(
-                        v_stack()
+                        v_flex()
                             .gap_2()
                             .p_4()
                             .child(
@@ -592,7 +592,7 @@ impl Render for NotificationPanel {
                     )
                 } else if self.notification_list.item_count() == 0 {
                     this.child(
-                        v_stack().p_4().child(
+                        v_flex().p_4().child(
                             div().flex().w_full().items_center().child(
                                 Label::new("You have no notifications.")
                                     .color(Color::Muted)
@@ -711,7 +711,7 @@ impl Render for NotificationToast {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
         let user = self.actor.clone();
 
-        h_stack()
+        h_flex()
             .id("notification_panel_toast")
             .children(user.map(|user| Avatar::new(user.avatar_uri.clone())))
             .child(Label::new(self.text.clone()))

crates/collab_ui/src/notifications/collab_notification.rs 🔗

@@ -33,7 +33,7 @@ impl ParentElement for CollabNotification {
 
 impl RenderOnce for CollabNotification {
     fn render(self, cx: &mut WindowContext) -> impl IntoElement {
-        h_stack()
+        h_flex()
             .text_ui()
             .justify_between()
             .size_full()
@@ -42,9 +42,9 @@ impl RenderOnce for CollabNotification {
             .p_2()
             .gap_2()
             .child(img(self.avatar_uri).w_12().h_12().rounded_full())
-            .child(v_stack().overflow_hidden().children(self.children))
+            .child(v_flex().overflow_hidden().children(self.children))
             .child(
-                v_stack()
+                v_flex()
                     .child(self.accept_button)
                     .child(self.dismiss_button),
             )

crates/collab_ui/src/notifications/incoming_call_notification.rs 🔗

@@ -137,7 +137,7 @@ impl Render for IncomingCallNotification {
                     move |_, cx| state.respond(false, cx)
                 }),
             )
-            .child(v_stack().overflow_hidden().child(Label::new(format!(
+            .child(v_flex().overflow_hidden().child(Label::new(format!(
                 "{} is sharing a project in Zed",
                 self.state.call.calling_user.github_login
             )))),

crates/command_palette/Cargo.toml 🔗

@@ -9,6 +9,7 @@ path = "src/command_palette.rs"
 doctest = false
 
 [dependencies]
+client = { path = "../client" }
 collections = { path = "../collections" }
 editor = { path = "../editor" }
 fuzzy = {  path = "../fuzzy" }
@@ -16,9 +17,9 @@ gpui = { path = "../gpui" }
 picker = { path = "../picker" }
 project = { path = "../project" }
 settings = { path = "../settings" }
+theme = { path = "../theme" }
 ui = { path = "../ui" }
 util = { path = "../util" }
-theme = { path = "../theme" }
 workspace = { path = "../workspace" }
 zed_actions = { path = "../zed_actions" }
 anyhow.workspace = true

crates/command_palette/src/command_palette.rs 🔗

@@ -3,6 +3,7 @@ use std::{
     sync::Arc,
 };
 
+use client::telemetry::Telemetry;
 use collections::{CommandPaletteFilter, HashMap};
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
@@ -11,7 +12,7 @@ use gpui::{
 };
 use picker::{Picker, PickerDelegate};
 
-use ui::{h_stack, prelude::*, v_stack, HighlightedLabel, KeyBinding, ListItem, ListItemSpacing};
+use ui::{h_flex, prelude::*, v_flex, HighlightedLabel, KeyBinding, ListItem, ListItemSpacing};
 use util::{
     channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL},
     ResultExt,
@@ -39,11 +40,18 @@ impl CommandPalette {
             let Some(previous_focus_handle) = cx.focused() else {
                 return;
             };
-            workspace.toggle_modal(cx, move |cx| CommandPalette::new(previous_focus_handle, cx));
+            let telemetry = workspace.client().telemetry().clone();
+            workspace.toggle_modal(cx, move |cx| {
+                CommandPalette::new(previous_focus_handle, telemetry, cx)
+            });
         });
     }
 
-    fn new(previous_focus_handle: FocusHandle, cx: &mut ViewContext<Self>) -> Self {
+    fn new(
+        previous_focus_handle: FocusHandle,
+        telemetry: Arc<Telemetry>,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
         let filter = cx.try_global::<CommandPaletteFilter>();
 
         let commands = cx
@@ -66,8 +74,12 @@ impl CommandPalette {
             })
             .collect();
 
-        let delegate =
-            CommandPaletteDelegate::new(cx.view().downgrade(), commands, previous_focus_handle);
+        let delegate = CommandPaletteDelegate::new(
+            cx.view().downgrade(),
+            commands,
+            telemetry,
+            previous_focus_handle,
+        );
 
         let picker = cx.new_view(|cx| Picker::new(delegate, cx));
         Self { picker }
@@ -84,7 +96,7 @@ impl FocusableView for CommandPalette {
 
 impl Render for CommandPalette {
     fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
-        v_stack().w(rems(34.)).child(self.picker.clone())
+        v_flex().w(rems(34.)).child(self.picker.clone())
     }
 }
 
@@ -103,6 +115,7 @@ pub struct CommandPaletteDelegate {
     commands: Vec<Command>,
     matches: Vec<StringMatch>,
     selected_ix: usize,
+    telemetry: Arc<Telemetry>,
     previous_focus_handle: FocusHandle,
 }
 
@@ -130,6 +143,7 @@ impl CommandPaletteDelegate {
     fn new(
         command_palette: WeakView<CommandPalette>,
         commands: Vec<Command>,
+        telemetry: Arc<Telemetry>,
         previous_focus_handle: FocusHandle,
     ) -> Self {
         Self {
@@ -138,6 +152,7 @@ impl CommandPaletteDelegate {
             matches: vec![],
             commands,
             selected_ix: 0,
+            telemetry,
             previous_focus_handle,
         }
     }
@@ -284,6 +299,10 @@ impl PickerDelegate for CommandPaletteDelegate {
         }
         let action_ix = self.matches[self.selected_ix].candidate_id;
         let command = self.commands.swap_remove(action_ix);
+
+        self.telemetry
+            .report_action_event("command palette", command.name.clone());
+
         self.matches.clear();
         self.commands.clear();
         cx.update_global(|hit_counts: &mut HitCounts, _| {
@@ -311,7 +330,7 @@ impl PickerDelegate for CommandPaletteDelegate {
                 .spacing(ListItemSpacing::Sparse)
                 .selected(selected)
                 .child(
-                    h_stack()
+                    h_flex()
                         .w_full()
                         .justify_between()
                         .child(HighlightedLabel::new(

crates/copilot_ui/src/sign_in.rs 🔗

@@ -57,7 +57,7 @@ impl CopilotCodeVerification {
             .read_from_clipboard()
             .map(|item| item.text() == &data.user_code)
             .unwrap_or(false);
-        h_stack()
+        h_flex()
             .w_full()
             .p_1()
             .border()
@@ -69,7 +69,7 @@ impl CopilotCodeVerification {
                 let user_code = data.user_code.clone();
                 move |_, cx| {
                     cx.write_to_clipboard(ClipboardItem::new(user_code.clone()));
-                    cx.notify();
+                    cx.refresh();
                 }
             })
             .child(div().flex_1().child(Label::new(data.user_code.clone())))
@@ -90,7 +90,7 @@ impl CopilotCodeVerification {
         } else {
             "Connect to Github"
         };
-        v_stack()
+        v_flex()
             .flex_1()
             .gap_2()
             .items_center()
@@ -118,7 +118,7 @@ impl CopilotCodeVerification {
             )
     }
     fn render_enabled_modal(cx: &mut ViewContext<Self>) -> impl Element {
-        v_stack()
+        v_flex()
             .gap_2()
             .child(Headline::new("Copilot Enabled!").size(HeadlineSize::Large))
             .child(Label::new(
@@ -132,7 +132,7 @@ impl CopilotCodeVerification {
     }
 
     fn render_unauthorized_modal() -> impl Element {
-        v_stack()
+        v_flex()
             .child(Headline::new("You must have an active GitHub Copilot subscription.").size(HeadlineSize::Large))
 
             .child(Label::new(
@@ -163,7 +163,7 @@ impl Render for CopilotCodeVerification {
             _ => div().into_any_element(),
         };
 
-        v_stack()
+        v_flex()
             .id("copilot code verification")
             .elevation_3(cx)
             .w_96()

crates/diagnostics/src/diagnostics.rs 🔗

@@ -36,7 +36,7 @@ use std::{
 };
 use theme::ActiveTheme;
 pub use toolbar_controls::ToolbarControls;
-use ui::{h_stack, prelude::*, Icon, IconName, Label};
+use ui::{h_flex, prelude::*, Icon, IconName, Label};
 use util::TryFutureExt;
 use workspace::{
     item::{BreadcrumbText, Item, ItemEvent, ItemHandle},
@@ -654,11 +654,11 @@ impl Item for ProjectDiagnosticsEditor {
                 })
                 .into_any_element()
         } else {
-            h_stack()
+            h_flex()
                 .gap_1()
                 .when(self.summary.error_count > 0, |then| {
                     then.child(
-                        h_stack()
+                        h_flex()
                             .gap_1()
                             .child(Icon::new(IconName::XCircle).color(Color::Error))
                             .child(Label::new(self.summary.error_count.to_string()).color(
@@ -672,7 +672,7 @@ impl Item for ProjectDiagnosticsEditor {
                 })
                 .when(self.summary.warning_count > 0, |then| {
                     then.child(
-                        h_stack()
+                        h_flex()
                             .gap_1()
                             .child(Icon::new(IconName::ExclamationTriangle).color(Color::Warning))
                             .child(Label::new(self.summary.warning_count.to_string()).color(
@@ -688,6 +688,10 @@ impl Item for ProjectDiagnosticsEditor {
         }
     }
 
+    fn telemetry_event_text(&self) -> Option<&'static str> {
+        Some("project diagnostics")
+    }
+
     fn for_each_project_item(
         &self,
         cx: &AppContext,
@@ -796,7 +800,7 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
     let message: SharedString = message.into();
     Arc::new(move |cx| {
         let highlight_style: HighlightStyle = cx.theme().colors().text_accent.into();
-        h_stack()
+        h_flex()
             .id("diagnostic header")
             .py_2()
             .pl_10()
@@ -805,7 +809,7 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
             .justify_between()
             .gap_2()
             .child(
-                h_stack()
+                h_flex()
                     .gap_3()
                     .map(|stack| {
                         stack.child(
@@ -824,7 +828,7 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
                         )
                     })
                     .child(
-                        h_stack()
+                        h_flex()
                             .gap_1()
                             .child(
                                 StyledText::new(message.clone()).with_highlights(
@@ -844,7 +848,7 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
                     ),
             )
             .child(
-                h_stack()
+                h_flex()
                     .gap_1()
                     .when_some(diagnostic.source.as_ref(), |stack, source| {
                         stack.child(

crates/diagnostics/src/items.rs 🔗

@@ -6,7 +6,7 @@ use gpui::{
 };
 use language::Diagnostic;
 use lsp::LanguageServerId;
-use ui::{h_stack, prelude::*, Button, ButtonLike, Color, Icon, IconName, Label, Tooltip};
+use ui::{h_flex, prelude::*, Button, ButtonLike, Color, Icon, IconName, Label, Tooltip};
 use workspace::{item::ItemHandle, StatusItemView, ToolbarItemEvent, Workspace};
 
 use crate::{Deploy, ProjectDiagnosticsEditor};
@@ -23,14 +23,14 @@ pub struct DiagnosticIndicator {
 impl Render for DiagnosticIndicator {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
         let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) {
-            (0, 0) => h_stack().map(|this| {
+            (0, 0) => h_flex().map(|this| {
                 this.child(
                     Icon::new(IconName::Check)
                         .size(IconSize::Small)
                         .color(Color::Default),
                 )
             }),
-            (0, warning_count) => h_stack()
+            (0, warning_count) => h_flex()
                 .gap_1()
                 .child(
                     Icon::new(IconName::ExclamationTriangle)
@@ -38,7 +38,7 @@ impl Render for DiagnosticIndicator {
                         .color(Color::Warning),
                 )
                 .child(Label::new(warning_count.to_string()).size(LabelSize::Small)),
-            (error_count, 0) => h_stack()
+            (error_count, 0) => h_flex()
                 .gap_1()
                 .child(
                     Icon::new(IconName::XCircle)
@@ -46,7 +46,7 @@ impl Render for DiagnosticIndicator {
                         .color(Color::Error),
                 )
                 .child(Label::new(error_count.to_string()).size(LabelSize::Small)),
-            (error_count, warning_count) => h_stack()
+            (error_count, warning_count) => h_flex()
                 .gap_1()
                 .child(
                     Icon::new(IconName::XCircle)
@@ -64,7 +64,7 @@ impl Render for DiagnosticIndicator {
 
         let status = if !self.in_progress_checks.is_empty() {
             Some(
-                h_stack()
+                h_flex()
                     .gap_2()
                     .child(Icon::new(IconName::ArrowCircle).size(IconSize::Small))
                     .child(
@@ -91,7 +91,7 @@ impl Render for DiagnosticIndicator {
             None
         };
 
-        h_stack()
+        h_flex()
             .h(rems(1.375))
             .gap_2()
             .child(

crates/editor/src/editor.rs 🔗

@@ -97,10 +97,13 @@ use std::{
 pub use sum_tree::Bias;
 use sum_tree::TreeMap;
 use text::{OffsetUtf16, Rope};
-use theme::{ActiveTheme, PlayerColor, StatusColors, SyntaxTheme, ThemeColors, ThemeSettings};
+use theme::{
+    observe_buffer_font_size_adjustment, ActiveTheme, PlayerColor, StatusColors, SyntaxTheme,
+    ThemeColors, ThemeSettings,
+};
 use ui::{
-    h_stack, prelude::*, ButtonSize, ButtonStyle, IconButton, IconName, IconSize, ListItem,
-    Popover, Tooltip,
+    h_flex, prelude::*, ButtonSize, ButtonStyle, IconButton, IconName, IconSize, ListItem, Popover,
+    Tooltip,
 };
 use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
 use workspace::{searchable::SearchEvent, ItemNavHistory, Pane, SplitDirection, ViewId, Workspace};
@@ -604,6 +607,7 @@ pub struct Editor {
     gutter_width: Pixels,
     style: Option<EditorStyle>,
     editor_actions: Vec<Box<dyn Fn(&mut ViewContext<Self>)>>,
+    show_copilot_suggestions: bool,
 }
 
 pub struct EditorSnapshot {
@@ -1263,7 +1267,7 @@ impl CompletionsMenu {
                                     None
                                 } else {
                                     Some(
-                                        h_stack().ml_4().child(
+                                        h_flex().ml_4().child(
                                             Label::new(text.clone())
                                                 .size(LabelSize::Small)
                                                 .color(Color::Muted),
@@ -1289,7 +1293,7 @@ impl CompletionsMenu {
                                         )
                                         .map(|task| task.detach_and_log_err(cx));
                                 }))
-                                .child(h_stack().overflow_hidden().child(completion_label))
+                                .child(h_flex().overflow_hidden().child(completion_label))
                                 .end_slot::<Div>(documentation_label),
                         )
                     })
@@ -1804,12 +1808,14 @@ impl Editor {
             gutter_width: Default::default(),
             style: None,
             editor_actions: Default::default(),
+            show_copilot_suggestions: mode == EditorMode::Full,
             _subscriptions: vec![
                 cx.observe(&buffer, Self::on_buffer_changed),
                 cx.subscribe(&buffer, Self::on_buffer_event),
                 cx.observe(&display_map, Self::on_display_map_changed),
                 cx.observe(&blink_manager, |_, _, cx| cx.notify()),
                 cx.observe_global::<SettingsStore>(Self::settings_changed),
+                observe_buffer_font_size_adjustment(cx, |_, cx| cx.notify()),
                 cx.observe_window_activation(|editor, cx| {
                     let active = cx.is_window_active();
                     editor.blink_manager.update(cx, |blink_manager, cx| {
@@ -1955,17 +1961,21 @@ impl Editor {
         }
     }
 
-    //     pub fn language_at<'a, T: ToOffset>(
-    //         &self,
-    //         point: T,
-    //         cx: &'a AppContext,
-    //     ) -> Option<Arc<Language>> {
-    //         self.buffer.read(cx).language_at(point, cx)
-    //     }
+    pub fn language_at<'a, T: ToOffset>(
+        &self,
+        point: T,
+        cx: &'a AppContext,
+    ) -> Option<Arc<Language>> {
+        self.buffer.read(cx).language_at(point, cx)
+    }
 
-    //     pub fn file_at<'a, T: ToOffset>(&self, point: T, cx: &'a AppContext) -> Option<Arc<dyn File>> {
-    //         self.buffer.read(cx).read(cx).file_at(point).cloned()
-    //     }
+    pub fn file_at<'a, T: ToOffset>(
+        &self,
+        point: T,
+        cx: &'a AppContext,
+    ) -> Option<Arc<dyn language::File>> {
+        self.buffer.read(cx).read(cx).file_at(point).cloned()
+    }
 
     pub fn active_excerpt(
         &self,
@@ -1976,15 +1986,6 @@ impl Editor {
             .excerpt_containing(self.selections.newest_anchor().head(), cx)
     }
 
-    //     pub fn style(&self, cx: &AppContext) -> EditorStyle {
-    //         build_style(
-    //             settings::get::<ThemeSettings>(cx),
-    //             self.get_field_editor_theme.as_deref(),
-    //             self.override_text_style.as_deref(),
-    //             cx,
-    //         )
-    //     }
-
     pub fn mode(&self) -> EditorMode {
         self.mode
     }
@@ -2071,6 +2072,10 @@ impl Editor {
         self.read_only = read_only;
     }
 
+    pub fn set_show_copilot_suggestions(&mut self, show_copilot_suggestions: bool) {
+        self.show_copilot_suggestions = show_copilot_suggestions;
+    }
+
     fn selections_did_change(
         &mut self,
         local: bool,
@@ -3981,7 +3986,7 @@ impl Editor {
         cx: &mut ViewContext<Self>,
     ) -> Option<()> {
         let copilot = Copilot::global(cx)?;
-        if self.mode != EditorMode::Full || !copilot.read(cx).status().is_authorized() {
+        if !self.show_copilot_suggestions || !copilot.read(cx).status().is_authorized() {
             self.clear_copilot_suggestions(cx);
             return None;
         }
@@ -4041,7 +4046,7 @@ impl Editor {
         cx: &mut ViewContext<Self>,
     ) -> Option<()> {
         let copilot = Copilot::global(cx)?;
-        if self.mode != EditorMode::Full || !copilot.read(cx).status().is_authorized() {
+        if !self.show_copilot_suggestions || !copilot.read(cx).status().is_authorized() {
             return None;
         }
 
@@ -4166,7 +4171,8 @@ impl Editor {
         let file = snapshot.file_at(location);
         let language = snapshot.language_at(location);
         let settings = all_language_settings(file, cx);
-        settings.copilot_enabled(language, file.map(|f| f.path().as_ref()))
+        self.show_copilot_suggestions
+            && settings.copilot_enabled(language, file.map(|f| f.path().as_ref()))
     }
 
     fn has_active_copilot_suggestion(&self, cx: &AppContext) -> bool {
@@ -4511,7 +4517,7 @@ impl Editor {
     }
 
     pub fn tab(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
-        if self.move_to_next_snippet_tabstop(cx) {
+        if self.move_to_next_snippet_tabstop(cx) || self.read_only(cx) {
             return;
         }
 
@@ -5443,6 +5449,10 @@ impl Editor {
     }
 
     pub fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
+        if self.read_only(cx) {
+            return;
+        }
+
         self.transact(cx, |this, cx| {
             if let Some(item) = cx.read_from_clipboard() {
                 let clipboard_text = Cow::Borrowed(item.text());
@@ -5515,6 +5525,10 @@ impl Editor {
     }
 
     pub fn undo(&mut self, _: &Undo, cx: &mut ViewContext<Self>) {
+        if self.read_only(cx) {
+            return;
+        }
+
         if let Some(tx_id) = self.buffer.update(cx, |buffer, cx| buffer.undo(cx)) {
             if let Some((selections, _)) = self.selection_history.transaction(tx_id).cloned() {
                 self.change_selections(None, cx, |s| {
@@ -5529,6 +5543,10 @@ impl Editor {
     }
 
     pub fn redo(&mut self, _: &Redo, cx: &mut ViewContext<Self>) {
+        if self.read_only(cx) {
+            return;
+        }
+
         if let Some(tx_id) = self.buffer.update(cx, |buffer, cx| buffer.redo(cx)) {
             if let Some((_, Some(selections))) = self.selection_history.transaction(tx_id).cloned()
             {
@@ -6453,42 +6471,79 @@ impl Editor {
             }
 
             self.select_next_state = Some(select_next_state);
-        } else if selections.len() == 1 {
-            let selection = selections.last_mut().unwrap();
-            if selection.start == selection.end {
-                let word_range = movement::surrounding_word(
-                    &display_map,
-                    selection.start.to_display_point(&display_map),
-                );
-                selection.start = word_range.start.to_offset(&display_map, Bias::Left);
-                selection.end = word_range.end.to_offset(&display_map, Bias::Left);
-                selection.goal = SelectionGoal::None;
-                selection.reversed = false;
-
-                let query = buffer
-                    .text_for_range(selection.start..selection.end)
-                    .collect::<String>();
-
-                let is_empty = query.is_empty();
-                let select_state = SelectNextState {
-                    query: AhoCorasick::new(&[query])?,
-                    wordwise: true,
-                    done: is_empty,
-                };
-                select_next_match_ranges(
-                    self,
-                    selection.start..selection.end,
-                    replace_newest,
-                    autoscroll,
-                    cx,
-                );
-                self.select_next_state = Some(select_state);
-            } else {
-                let query = buffer
-                    .text_for_range(selection.start..selection.end)
-                    .collect::<String>();
+        } else {
+            let mut only_carets = true;
+            let mut same_text_selected = true;
+            let mut selected_text = None;
+
+            let mut selections_iter = selections.iter().peekable();
+            while let Some(selection) = selections_iter.next() {
+                if selection.start != selection.end {
+                    only_carets = false;
+                }
+
+                if same_text_selected {
+                    if selected_text.is_none() {
+                        selected_text =
+                            Some(buffer.text_for_range(selection.range()).collect::<String>());
+                    }
+
+                    if let Some(next_selection) = selections_iter.peek() {
+                        if next_selection.range().len() == selection.range().len() {
+                            let next_selected_text = buffer
+                                .text_for_range(next_selection.range())
+                                .collect::<String>();
+                            if Some(next_selected_text) != selected_text {
+                                same_text_selected = false;
+                                selected_text = None;
+                            }
+                        } else {
+                            same_text_selected = false;
+                            selected_text = None;
+                        }
+                    }
+                }
+            }
+
+            if only_carets {
+                for selection in &mut selections {
+                    let word_range = movement::surrounding_word(
+                        &display_map,
+                        selection.start.to_display_point(&display_map),
+                    );
+                    selection.start = word_range.start.to_offset(&display_map, Bias::Left);
+                    selection.end = word_range.end.to_offset(&display_map, Bias::Left);
+                    selection.goal = SelectionGoal::None;
+                    selection.reversed = false;
+                    select_next_match_ranges(
+                        self,
+                        selection.start..selection.end,
+                        replace_newest,
+                        autoscroll,
+                        cx,
+                    );
+                }
+
+                if selections.len() == 1 {
+                    let selection = selections
+                        .last()
+                        .expect("ensured that there's only one selection");
+                    let query = buffer
+                        .text_for_range(selection.start..selection.end)
+                        .collect::<String>();
+                    let is_empty = query.is_empty();
+                    let select_state = SelectNextState {
+                        query: AhoCorasick::new(&[query])?,
+                        wordwise: true,
+                        done: is_empty,
+                    };
+                    self.select_next_state = Some(select_state);
+                } else {
+                    self.select_next_state = None;
+                }
+            } else if let Some(selected_text) = selected_text {
                 self.select_next_state = Some(SelectNextState {
-                    query: AhoCorasick::new(&[query])?,
+                    query: AhoCorasick::new(&[selected_text])?,
                     wordwise: false,
                     done: false,
                 });
@@ -6592,39 +6647,81 @@ impl Editor {
             }
 
             self.select_prev_state = Some(select_prev_state);
-        } else if selections.len() == 1 {
-            let selection = selections.last_mut().unwrap();
-            if selection.start == selection.end {
-                let word_range = movement::surrounding_word(
-                    &display_map,
-                    selection.start.to_display_point(&display_map),
+        } else {
+            let mut only_carets = true;
+            let mut same_text_selected = true;
+            let mut selected_text = None;
+
+            let mut selections_iter = selections.iter().peekable();
+            while let Some(selection) = selections_iter.next() {
+                if selection.start != selection.end {
+                    only_carets = false;
+                }
+
+                if same_text_selected {
+                    if selected_text.is_none() {
+                        selected_text =
+                            Some(buffer.text_for_range(selection.range()).collect::<String>());
+                    }
+
+                    if let Some(next_selection) = selections_iter.peek() {
+                        if next_selection.range().len() == selection.range().len() {
+                            let next_selected_text = buffer
+                                .text_for_range(next_selection.range())
+                                .collect::<String>();
+                            if Some(next_selected_text) != selected_text {
+                                same_text_selected = false;
+                                selected_text = None;
+                            }
+                        } else {
+                            same_text_selected = false;
+                            selected_text = None;
+                        }
+                    }
+                }
+            }
+
+            if only_carets {
+                for selection in &mut selections {
+                    let word_range = movement::surrounding_word(
+                        &display_map,
+                        selection.start.to_display_point(&display_map),
+                    );
+                    selection.start = word_range.start.to_offset(&display_map, Bias::Left);
+                    selection.end = word_range.end.to_offset(&display_map, Bias::Left);
+                    selection.goal = SelectionGoal::None;
+                    selection.reversed = false;
+                }
+                if selections.len() == 1 {
+                    let selection = selections
+                        .last()
+                        .expect("ensured that there's only one selection");
+                    let query = buffer
+                        .text_for_range(selection.start..selection.end)
+                        .collect::<String>();
+                    let is_empty = query.is_empty();
+                    let select_state = SelectNextState {
+                        query: AhoCorasick::new(&[query.chars().rev().collect::<String>()])?,
+                        wordwise: true,
+                        done: is_empty,
+                    };
+                    self.select_prev_state = Some(select_state);
+                } else {
+                    self.select_prev_state = None;
+                }
+
+                self.unfold_ranges(
+                    selections.iter().map(|s| s.range()).collect::<Vec<_>>(),
+                    false,
+                    true,
+                    cx,
                 );
-                selection.start = word_range.start.to_offset(&display_map, Bias::Left);
-                selection.end = word_range.end.to_offset(&display_map, Bias::Left);
-                selection.goal = SelectionGoal::None;
-                selection.reversed = false;
-
-                let query = buffer
-                    .text_for_range(selection.start..selection.end)
-                    .collect::<String>();
-                let query = query.chars().rev().collect::<String>();
-                let select_state = SelectNextState {
-                    query: AhoCorasick::new(&[query])?,
-                    wordwise: true,
-                    done: false,
-                };
-                self.unfold_ranges([selection.start..selection.end], false, true, cx);
                 self.change_selections(Some(Autoscroll::newest()), cx, |s| {
                     s.select(selections);
                 });
-                self.select_prev_state = Some(select_state);
-            } else {
-                let query = buffer
-                    .text_for_range(selection.start..selection.end)
-                    .collect::<String>();
-                let query = query.chars().rev().collect::<String>();
+            } else if let Some(selected_text) = selected_text {
                 self.select_prev_state = Some(SelectNextState {
-                    query: AhoCorasick::new(&[query])?,
+                    query: AhoCorasick::new(&[selected_text.chars().rev().collect::<String>()])?,
                     wordwise: false,
                     done: false,
                 });
@@ -8727,6 +8824,7 @@ impl Editor {
             )),
             cx,
         );
+        cx.notify();
     }
 
     pub fn set_searchable(&mut self, searchable: bool) {
@@ -9733,7 +9831,7 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, _is_valid: bool) -> Ren
         let group_id: SharedString = cx.block_id.to_string().into();
         // TODO: Nate: We should tint the background of the block with the severity color
         // We need to extend the theme before we can do this
-        h_stack()
+        h_flex()
             .id(cx.block_id)
             .group(group_id.clone())
             .relative()

crates/editor/src/editor_tests.rs 🔗

@@ -3821,62 +3821,137 @@ async fn test_select_next(cx: &mut gpui::TestAppContext) {
 }
 
 #[gpui::test]
-async fn test_select_previous(cx: &mut gpui::TestAppContext) {
+async fn test_select_next_with_multiple_carets(cx: &mut gpui::TestAppContext) {
     init_test(cx, |_| {});
-    {
-        // `Select previous` without a selection (selects wordwise)
-        let mut cx = EditorTestContext::new(cx).await;
-        cx.set_state("abc\nˇabc abc\ndefabc\nabc");
 
-        cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
-            .unwrap();
-        cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
+    let mut cx = EditorTestContext::new(cx).await;
+    cx.set_state(
+        r#"let foo = 2;
+lˇet foo = 2;
+let fooˇ = 2;
+let foo = 2;
+let foo = ˇ2;"#,
+    );
 
-        cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
-            .unwrap();
-        cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
+    cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
+        .unwrap();
+    cx.assert_editor_state(
+        r#"let foo = 2;
+«letˇ» foo = 2;
+let «fooˇ» = 2;
+let foo = 2;
+let foo = «2ˇ»;"#,
+    );
 
-        cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx));
-        cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
+    // noop for multiple selections with different contents
+    cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
+        .unwrap();
+    cx.assert_editor_state(
+        r#"let foo = 2;
+«letˇ» foo = 2;
+let «fooˇ» = 2;
+let foo = 2;
+let foo = «2ˇ»;"#,
+    );
+}
 
-        cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx));
-        cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
+#[gpui::test]
+async fn test_select_previous_with_single_caret(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
 
-        cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
-            .unwrap();
-        cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
+    let mut cx = EditorTestContext::new(cx).await;
+    cx.set_state("abc\nˇabc abc\ndefabc\nabc");
 
-        cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
-            .unwrap();
-        cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
-    }
-    {
-        // `Select previous` with a selection
-        let mut cx = EditorTestContext::new(cx).await;
-        cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
+    cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
+        .unwrap();
+    cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
 
-        cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
-            .unwrap();
-        cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
+    cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
+        .unwrap();
+    cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
 
-        cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
-            .unwrap();
-        cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
+    cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx));
+    cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
 
-        cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx));
-        cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
+    cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx));
+    cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
 
-        cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx));
-        cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
+    cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
+        .unwrap();
+    cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
 
-        cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
-            .unwrap();
-        cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»");
+    cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
+        .unwrap();
+    cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndef«abcˇ»\n«abcˇ»");
 
-        cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
-            .unwrap();
-        cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
-    }
+    cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
+        .unwrap();
+    cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
+}
+
+#[gpui::test]
+async fn test_select_previous_with_multiple_carets(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorTestContext::new(cx).await;
+    cx.set_state(
+        r#"let foo = 2;
+lˇet foo = 2;
+let fooˇ = 2;
+let foo = 2;
+let foo = ˇ2;"#,
+    );
+
+    cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
+        .unwrap();
+    cx.assert_editor_state(
+        r#"let foo = 2;
+«letˇ» foo = 2;
+let «fooˇ» = 2;
+let foo = 2;
+let foo = «2ˇ»;"#,
+    );
+
+    // noop for multiple selections with different contents
+    cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
+        .unwrap();
+    cx.assert_editor_state(
+        r#"let foo = 2;
+«letˇ» foo = 2;
+let «fooˇ» = 2;
+let foo = 2;
+let foo = «2ˇ»;"#,
+    );
+}
+
+#[gpui::test]
+async fn test_select_previous_with_single_selection(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
+    let mut cx = EditorTestContext::new(cx).await;
+    cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
+
+    cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
+        .unwrap();
+    cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
+
+    cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
+        .unwrap();
+    cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
+
+    cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx));
+    cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
+
+    cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx));
+    cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
+
+    cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
+        .unwrap();
+    cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»");
+
+    cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
+        .unwrap();
+    cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
 }
 
 #[gpui::test]

crates/editor/src/element.rs 🔗

@@ -26,11 +26,11 @@ use git::diff::DiffHunkStatus;
 use gpui::{
     div, fill, outline, overlay, point, px, quad, relative, size, transparent_black, Action,
     AnchorCorner, AnyElement, AvailableSpace, BorrowWindow, Bounds, ContentMask, Corners,
-    CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Hsla, InteractiveBounds,
-    InteractiveElement, IntoElement, ModifiersChangedEvent, MouseButton, MouseDownEvent,
-    MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine,
-    SharedString, Size, StackingOrder, StatefulInteractiveElement, Style, Styled, TextRun,
-    TextStyle, View, ViewContext, WindowContext,
+    CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity, Hsla,
+    InteractiveBounds, InteractiveElement, IntoElement, ModifiersChangedEvent, MouseButton,
+    MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, ScrollDelta,
+    ScrollWheelEvent, ShapedLine, SharedString, Size, StackingOrder, StatefulInteractiveElement,
+    Style, Styled, TextRun, TextStyle, View, ViewContext, WindowContext,
 };
 use itertools::Itertools;
 use language::language_settings::ShowWhitespaceSetting;
@@ -53,7 +53,7 @@ use std::{
 use sum_tree::Bias;
 use theme::{ActiveTheme, PlayerColor};
 use ui::prelude::*;
-use ui::{h_stack, ButtonLike, ButtonStyle, IconButton, Tooltip};
+use ui::{h_flex, ButtonLike, ButtonStyle, IconButton, Tooltip};
 use util::ResultExt;
 use workspace::item::Item;
 
@@ -388,7 +388,9 @@ impl EditorElement {
         let mut click_count = event.click_count;
         let modifiers = event.modifiers;
 
-        if gutter_bounds.contains(&event.position) {
+        if cx.default_prevented() {
+            return;
+        } else if gutter_bounds.contains(&event.position) {
             click_count = 3; // Simulate triple-click when clicking the gutter to select lines
         } else if !text_bounds.contains(&event.position) {
             return;
@@ -2269,11 +2271,9 @@ impl EditorElement {
                             .map_or(range.context.start, |primary| primary.start);
                         let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);
 
-                        let jump_handler = cx.listener_for(&self.editor, move |editor, _, cx| {
+                        cx.listener_for(&self.editor, move |editor, _, cx| {
                             editor.jump(jump_path.clone(), jump_position, jump_anchor, cx);
-                        });
-
-                        jump_handler
+                        })
                     });
 
                     let element = if *starts_new_buffer {
@@ -2293,7 +2293,7 @@ impl EditorElement {
                             .size_full()
                             .p_1p5()
                             .child(
-                                h_stack()
+                                h_flex()
                                     .id("path header block")
                                     .py_1p5()
                                     .pl_3()
@@ -2306,8 +2306,8 @@ impl EditorElement {
                                     .justify_between()
                                     .hover(|style| style.bg(cx.theme().colors().element_hover))
                                     .child(
-                                        h_stack().gap_3().child(
-                                            h_stack()
+                                        h_flex().gap_3().child(
+                                            h_flex()
                                                 .gap_2()
                                                 .child(
                                                     filename
@@ -2339,12 +2339,12 @@ impl EditorElement {
                                     }),
                             )
                     } else {
-                        h_stack()
+                        h_flex()
                             .id(("collapsed context", block_id))
                             .size_full()
                             .gap(gutter_padding)
                             .child(
-                                h_stack()
+                                h_flex()
                                     .justify_end()
                                     .flex_none()
                                     .w(gutter_width - gutter_padding)
@@ -2353,34 +2353,25 @@ impl EditorElement {
                                     .text_color(cx.theme().colors().editor_line_number)
                                     .child("..."),
                             )
-                            .map(|this| {
-                                if let Some(jump_handler) = jump_handler {
-                                    this.child(
-                                        ButtonLike::new("jump to collapsed context")
-                                            .style(ButtonStyle::Transparent)
-                                            .full_width()
-                                            .on_click(jump_handler)
-                                            .tooltip(|cx| {
-                                                Tooltip::for_action(
-                                                    "Jump to Buffer",
-                                                    &OpenExcerpts,
-                                                    cx,
-                                                )
-                                            })
-                                            .child(
-                                                div()
-                                                    .h_px()
-                                                    .w_full()
-                                                    .bg(cx.theme().colors().border_variant)
-                                                    .group_hover("", |style| {
-                                                        style.bg(cx.theme().colors().border)
-                                                    }),
-                                            ),
+                            .child(
+                                ButtonLike::new("jump to collapsed context")
+                                    .style(ButtonStyle::Transparent)
+                                    .full_width()
+                                    .child(
+                                        div()
+                                            .h_px()
+                                            .w_full()
+                                            .bg(cx.theme().colors().border_variant)
+                                            .group_hover("", |style| {
+                                                style.bg(cx.theme().colors().border)
+                                            }),
                                     )
-                                } else {
-                                    this.child(div().size_full().bg(gpui::green()))
-                                }
-                            })
+                                    .when_some(jump_handler, |this, jump_handler| {
+                                        this.on_click(jump_handler).tooltip(|cx| {
+                                            Tooltip::for_action("Jump to Buffer", &OpenExcerpts, cx)
+                                        })
+                                    }),
+                            )
                     };
                     element.into_any()
                 }
@@ -2812,44 +2803,49 @@ impl Element for EditorElement {
         _element_state: Option<Self::State>,
         cx: &mut gpui::WindowContext,
     ) -> (gpui::LayoutId, Self::State) {
-        self.editor.update(cx, |editor, cx| {
-            editor.set_style(self.style.clone(), cx);
-
-            let layout_id = match editor.mode {
-                EditorMode::SingleLine => {
-                    let rem_size = cx.rem_size();
-                    let mut style = Style::default();
-                    style.size.width = relative(1.).into();
-                    style.size.height = self.style.text.line_height_in_pixels(rem_size).into();
-                    cx.request_layout(&style, None)
-                }
-                EditorMode::AutoHeight { max_lines } => {
-                    let editor_handle = cx.view().clone();
-                    let max_line_number_width =
-                        self.max_line_number_width(&editor.snapshot(cx), cx);
-                    cx.request_measured_layout(Style::default(), move |known_dimensions, _, cx| {
-                        editor_handle
-                            .update(cx, |editor, cx| {
-                                compute_auto_height_layout(
-                                    editor,
-                                    max_lines,
-                                    max_line_number_width,
-                                    known_dimensions,
-                                    cx,
-                                )
-                            })
-                            .unwrap_or_default()
-                    })
-                }
-                EditorMode::Full => {
-                    let mut style = Style::default();
-                    style.size.width = relative(1.).into();
-                    style.size.height = relative(1.).into();
-                    cx.request_layout(&style, None)
-                }
-            };
+        cx.with_view_id(self.editor.entity_id(), |cx| {
+            self.editor.update(cx, |editor, cx| {
+                editor.set_style(self.style.clone(), cx);
+
+                let layout_id = match editor.mode {
+                    EditorMode::SingleLine => {
+                        let rem_size = cx.rem_size();
+                        let mut style = Style::default();
+                        style.size.width = relative(1.).into();
+                        style.size.height = self.style.text.line_height_in_pixels(rem_size).into();
+                        cx.request_layout(&style, None)
+                    }
+                    EditorMode::AutoHeight { max_lines } => {
+                        let editor_handle = cx.view().clone();
+                        let max_line_number_width =
+                            self.max_line_number_width(&editor.snapshot(cx), cx);
+                        cx.request_measured_layout(
+                            Style::default(),
+                            move |known_dimensions, _, cx| {
+                                editor_handle
+                                    .update(cx, |editor, cx| {
+                                        compute_auto_height_layout(
+                                            editor,
+                                            max_lines,
+                                            max_line_number_width,
+                                            known_dimensions,
+                                            cx,
+                                        )
+                                    })
+                                    .unwrap_or_default()
+                            },
+                        )
+                    }
+                    EditorMode::Full => {
+                        let mut style = Style::default();
+                        style.size.width = relative(1.).into();
+                        style.size.height = relative(1.).into();
+                        cx.request_layout(&style, None)
+                    }
+                };
 
-            (layout_id, ())
+                (layout_id, ())
+            })
         })
     }
 
@@ -2861,65 +2857,67 @@ impl Element for EditorElement {
     ) {
         let editor = self.editor.clone();
 
-        cx.with_text_style(
-            Some(gpui::TextStyleRefinement {
-                font_size: Some(self.style.text.font_size),
-                ..Default::default()
-            }),
-            |cx| {
-                let mut layout = self.compute_layout(bounds, cx);
-                let gutter_bounds = Bounds {
-                    origin: bounds.origin,
-                    size: layout.gutter_size,
-                };
-                let text_bounds = Bounds {
-                    origin: gutter_bounds.upper_right(),
-                    size: layout.text_size,
-                };
+        cx.paint_view(self.editor.entity_id(), |cx| {
+            cx.with_text_style(
+                Some(gpui::TextStyleRefinement {
+                    font_size: Some(self.style.text.font_size),
+                    ..Default::default()
+                }),
+                |cx| {
+                    let mut layout = self.compute_layout(bounds, cx);
+                    let gutter_bounds = Bounds {
+                        origin: bounds.origin,
+                        size: layout.gutter_size,
+                    };
+                    let text_bounds = Bounds {
+                        origin: gutter_bounds.upper_right(),
+                        size: layout.text_size,
+                    };
 
-                let focus_handle = editor.focus_handle(cx);
-                let key_context = self.editor.read(cx).key_context(cx);
-                cx.with_key_dispatch(Some(key_context), Some(focus_handle.clone()), |_, cx| {
-                    self.register_actions(cx);
-                    self.register_key_listeners(cx);
+                    let focus_handle = editor.focus_handle(cx);
+                    let key_context = self.editor.read(cx).key_context(cx);
+                    cx.with_key_dispatch(Some(key_context), Some(focus_handle.clone()), |_, cx| {
+                        self.register_actions(cx);
+                        self.register_key_listeners(cx);
 
-                    cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
-                        let input_handler =
-                            ElementInputHandler::new(bounds, self.editor.clone(), cx);
-                        cx.handle_input(&focus_handle, input_handler);
+                        cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
+                            let input_handler =
+                                ElementInputHandler::new(bounds, self.editor.clone(), cx);
+                            cx.handle_input(&focus_handle, input_handler);
 
-                        self.paint_background(gutter_bounds, text_bounds, &layout, cx);
-                        if layout.gutter_size.width > Pixels::ZERO {
-                            self.paint_gutter(gutter_bounds, &mut layout, cx);
-                        }
-                        self.paint_text(text_bounds, &mut layout, cx);
+                            self.paint_background(gutter_bounds, text_bounds, &layout, cx);
+                            if layout.gutter_size.width > Pixels::ZERO {
+                                self.paint_gutter(gutter_bounds, &mut layout, cx);
+                            }
+                            self.paint_text(text_bounds, &mut layout, cx);
 
-                        cx.with_z_index(0, |cx| {
-                            self.paint_mouse_listeners(
-                                bounds,
-                                gutter_bounds,
-                                text_bounds,
-                                &layout,
-                                cx,
-                            );
-                        });
-                        if !layout.blocks.is_empty() {
                             cx.with_z_index(0, |cx| {
-                                cx.with_element_id(Some("editor_blocks"), |cx| {
-                                    self.paint_blocks(bounds, &mut layout, cx);
-                                });
-                            })
-                        }
+                                self.paint_mouse_listeners(
+                                    bounds,
+                                    gutter_bounds,
+                                    text_bounds,
+                                    &layout,
+                                    cx,
+                                );
+                            });
+                            if !layout.blocks.is_empty() {
+                                cx.with_z_index(0, |cx| {
+                                    cx.with_element_id(Some("editor_blocks"), |cx| {
+                                        self.paint_blocks(bounds, &mut layout, cx);
+                                    });
+                                })
+                            }
 
-                        cx.with_z_index(1, |cx| {
-                            self.paint_overlays(text_bounds, &mut layout, cx);
-                        });
+                            cx.with_z_index(1, |cx| {
+                                self.paint_overlays(text_bounds, &mut layout, cx);
+                            });
 
-                        cx.with_z_index(2, |cx| self.paint_scrollbar(bounds, &mut layout, cx));
-                    });
-                })
-            },
-        );
+                            cx.with_z_index(2, |cx| self.paint_scrollbar(bounds, &mut layout, cx));
+                        });
+                    })
+                },
+            )
+        })
     }
 }
 
@@ -3415,14 +3413,16 @@ mod tests {
             })
             .unwrap();
         let state = cx
-            .update_window(window.into(), |_, cx| {
-                element.compute_layout(
-                    Bounds {
-                        origin: point(px(500.), px(500.)),
-                        size: size(px(500.), px(500.)),
-                    },
-                    cx,
-                )
+            .update_window(window.into(), |view, cx| {
+                cx.with_view_id(view.entity_id(), |cx| {
+                    element.compute_layout(
+                        Bounds {
+                            origin: point(px(500.), px(500.)),
+                            size: size(px(500.), px(500.)),
+                        },
+                        cx,
+                    )
+                })
             })
             .unwrap();
 
@@ -3507,14 +3507,16 @@ mod tests {
         });
 
         let state = cx
-            .update_window(window.into(), |_, cx| {
-                element.compute_layout(
-                    Bounds {
-                        origin: point(px(500.), px(500.)),
-                        size: size(px(500.), px(500.)),
-                    },
-                    cx,
-                )
+            .update_window(window.into(), |view, cx| {
+                cx.with_view_id(view.entity_id(), |cx| {
+                    element.compute_layout(
+                        Bounds {
+                            origin: point(px(500.), px(500.)),
+                            size: size(px(500.), px(500.)),
+                        },
+                        cx,
+                    )
+                })
             })
             .unwrap();
         assert_eq!(state.selections.len(), 1);
@@ -3569,14 +3571,16 @@ mod tests {
 
         let mut element = EditorElement::new(&editor, style);
         let state = cx
-            .update_window(window.into(), |_, cx| {
-                element.compute_layout(
-                    Bounds {
-                        origin: point(px(500.), px(500.)),
-                        size: size(px(500.), px(500.)),
-                    },
-                    cx,
-                )
+            .update_window(window.into(), |view, cx| {
+                cx.with_view_id(view.entity_id(), |cx| {
+                    element.compute_layout(
+                        Bounds {
+                            origin: point(px(500.), px(500.)),
+                            size: size(px(500.), px(500.)),
+                        },
+                        cx,
+                    )
+                })
             })
             .unwrap();
         let size = state.position_map.size;
@@ -3593,10 +3597,8 @@ mod tests {
 
         // Don't panic.
         let bounds = Bounds::<Pixels>::new(Default::default(), size);
-        cx.update_window(window.into(), |_, cx| {
-            element.paint(bounds, &mut (), cx);
-        })
-        .unwrap()
+        cx.update_window(window.into(), |_, cx| element.paint(bounds, &mut (), cx))
+            .unwrap()
     }
 
     #[gpui::test]

crates/editor/src/items.rs 🔗

@@ -32,7 +32,7 @@ use std::{
 };
 use text::Selection;
 use theme::Theme;
-use ui::{h_stack, prelude::*, Label};
+use ui::{h_flex, prelude::*, Label};
 use util::{paths::PathExt, paths::FILE_ROW_COLUMN_DELIMITER, ResultExt, TryFutureExt};
 use workspace::{
     item::{BreadcrumbText, FollowEvent, FollowableItemHandle},
@@ -578,6 +578,10 @@ impl Item for Editor {
         Some(file_path.into())
     }
 
+    fn telemetry_event_text(&self) -> Option<&'static str> {
+        None
+    }
+
     fn tab_description<'a>(&self, detail: usize, cx: &'a AppContext) -> Option<SharedString> {
         let path = path_for_buffer(&self.buffer, detail, true, cx)?;
         Some(path.to_string_lossy().to_string().into())
@@ -619,7 +623,7 @@ impl Item for Editor {
             Some(util::truncate_and_trailoff(&description, MAX_TAB_TITLE_LEN))
         });
 
-        h_stack()
+        h_flex()
             .gap_2()
             .child(Label::new(self.title(cx).to_string()).color(label_color))
             .when_some(description, |this, description| {

crates/editor/src/scroll/autoscroll.rs 🔗

@@ -5,7 +5,7 @@ use language::Point;
 
 use crate::{display_map::ToDisplayPoint, Editor, EditorMode, LineWithInvisibles};
 
-#[derive(PartialEq, Eq)]
+#[derive(PartialEq, Eq, Clone, Copy)]
 pub enum Autoscroll {
     Next,
     Strategy(AutoscrollStrategy),
@@ -25,7 +25,7 @@ impl Autoscroll {
     }
 }
 
-#[derive(PartialEq, Eq, Default)]
+#[derive(PartialEq, Eq, Default, Clone, Copy)]
 pub enum AutoscrollStrategy {
     Fit,
     Newest,

crates/feedback/src/feedback_modal.rs 🔗

@@ -186,6 +186,7 @@ impl FeedbackModal {
                 cx,
             );
             editor.set_show_gutter(false, cx);
+            editor.set_show_copilot_suggestions(false);
             editor.set_vertical_scroll_margin(5, cx);
             editor
         });
@@ -421,7 +422,7 @@ impl Render for FeedbackModal {
         let open_community_repo =
             cx.listener(|_, _, cx| cx.dispatch_action(Box::new(OpenZedCommunityRepo)));
 
-        v_stack()
+        v_flex()
             .elevation_3(cx)
             .key_context("GiveFeedback")
             .on_action(cx.listener(Self::cancel))
@@ -460,10 +461,10 @@ impl Render for FeedbackModal {
                     .child(self.feedback_editor.clone()),
             )
             .child(
-                v_stack()
+                v_flex()
                     .gap_1()
                     .child(
-                        h_stack()
+                        h_flex()
                             .bg(cx.theme().colors().editor_background)
                             .p_2()
                             .border()
@@ -482,7 +483,7 @@ impl Render for FeedbackModal {
                     ),
             )
             .child(
-                h_stack()
+                h_flex()
                     .justify_between()
                     .gap_1()
                     .child(
@@ -494,7 +495,7 @@ impl Render for FeedbackModal {
                             .on_click(open_community_repo),
                     )
                     .child(
-                        h_stack()
+                        h_flex()
                             .gap_1()
                             .child(
                                 Button::new("cancel_feedback", "Cancel")

crates/file_finder/src/file_finder.rs 🔗

@@ -119,7 +119,7 @@ impl FocusableView for FileFinder {
 }
 impl Render for FileFinder {
     fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
-        v_stack().w(rems(34.)).child(self.picker.clone())
+        v_flex().w(rems(34.)).child(self.picker.clone())
     }
 }
 
@@ -786,7 +786,7 @@ impl PickerDelegate for FileFinderDelegate {
                 .inset(true)
                 .selected(selected)
                 .child(
-                    v_stack()
+                    v_flex()
                         .child(HighlightedLabel::new(file_name, file_name_positions))
                         .child(HighlightedLabel::new(full_path, full_path_positions)),
                 ),

crates/go_to_line/src/go_to_line.rs 🔗

@@ -5,7 +5,7 @@ use gpui::{
 };
 use text::{Bias, Point};
 use theme::ActiveTheme;
-use ui::{h_stack, prelude::*, v_stack, Label};
+use ui::{h_flex, prelude::*, v_flex, Label};
 use util::paths::FILE_ROW_COLUMN_DELIMITER;
 use workspace::ModalView;
 
@@ -160,12 +160,12 @@ impl Render for GoToLine {
             .on_action(cx.listener(Self::confirm))
             .w_96()
             .child(
-                v_stack()
+                v_flex()
                     .px_1()
                     .pt_0p5()
                     .gap_px()
                     .child(
-                        v_stack()
+                        v_flex()
                             .py_0p5()
                             .px_1()
                             .child(div().px_1().py_0p5().child(self.line_editor.clone())),
@@ -177,7 +177,7 @@ impl Render for GoToLine {
                             .bg(cx.theme().colors().element_background),
                     )
                     .child(
-                        h_stack()
+                        h_flex()
                             .justify_between()
                             .px_2()
                             .py_1()

crates/gpui/Cargo.toml 🔗

@@ -78,7 +78,7 @@ cocoa = "0.24"
 core-foundation = { version = "0.9.3", features = ["with-uuid"] }
 core-graphics = "0.22.3"
 core-text = "19.2"
-font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "b2f77d56f450338aa4f7dd2f0197d8c9acb0cf18" }
+font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "d97147f" }
 foreign-types = "0.3"
 log.workspace = true
 metal = "0.21.0"

crates/gpui/build.rs 🔗

@@ -70,13 +70,23 @@ fn generate_shader_bindings() -> PathBuf {
     ]);
     config.no_includes = true;
     config.enumeration.prefix_with_name = true;
-    cbindgen::Builder::new()
-        .with_src(crate_dir.join("src/scene.rs"))
-        .with_src(crate_dir.join("src/geometry.rs"))
-        .with_src(crate_dir.join("src/color.rs"))
-        .with_src(crate_dir.join("src/window.rs"))
-        .with_src(crate_dir.join("src/platform.rs"))
-        .with_src(crate_dir.join("src/platform/mac/metal_renderer.rs"))
+
+    let mut builder = cbindgen::Builder::new();
+
+    let src_paths = [
+        crate_dir.join("src/scene.rs"),
+        crate_dir.join("src/geometry.rs"),
+        crate_dir.join("src/color.rs"),
+        crate_dir.join("src/window.rs"),
+        crate_dir.join("src/platform.rs"),
+        crate_dir.join("src/platform/mac/metal_renderer.rs"),
+    ];
+    for src_path in src_paths {
+        println!("cargo:rerun-if-changed={}", src_path.display());
+        builder = builder.with_src(src_path);
+    }
+
+    builder
         .with_config(config)
         .generate()
         .expect("Unable to generate bindings")

crates/gpui/src/app.rs 🔗

@@ -196,7 +196,6 @@ pub struct AppContext {
     pending_updates: usize,
     pub(crate) actions: Rc<ActionRegistry>,
     pub(crate) active_drag: Option<AnyDrag>,
-    pub(crate) active_tooltip: Option<AnyTooltip>,
     pub(crate) next_frame_callbacks: FxHashMap<DisplayId, Vec<FrameCallback>>,
     pub(crate) frame_consumers: FxHashMap<DisplayId, Task<()>>,
     pub(crate) background_executor: BackgroundExecutor,
@@ -258,7 +257,6 @@ impl AppContext {
                 flushing_effects: false,
                 pending_updates: 0,
                 active_drag: None,
-                active_tooltip: None,
                 next_frame_callbacks: FxHashMap::default(),
                 frame_consumers: FxHashMap::default(),
                 background_executor: executor,
@@ -845,6 +843,7 @@ impl AppContext {
     /// Remove the global of the given type from the app context. Does not notify global observers.
     pub fn remove_global<G: Any>(&mut self) -> G {
         let global_type = TypeId::of::<G>();
+        self.push_effect(Effect::NotifyGlobalObservers { global_type });
         *self
             .globals_by_type
             .remove(&global_type)
@@ -1268,8 +1267,10 @@ pub struct AnyDrag {
     pub cursor_offset: Point<Pixels>,
 }
 
+/// Contains state associated with a tooltip. You'll only need this struct if you're implementing
+/// tooltip behavior on a custom element. Otherwise, use [Div::tooltip].
 #[derive(Clone)]
-pub(crate) struct AnyTooltip {
+pub struct AnyTooltip {
     pub view: AnyView,
     pub cursor_offset: Point<Pixels>,
 }

crates/gpui/src/app/entity_map.rs 🔗

@@ -2,7 +2,7 @@ use crate::{seal::Sealed, AppContext, Context, Entity, ModelContext};
 use anyhow::{anyhow, Result};
 use derive_more::{Deref, DerefMut};
 use parking_lot::{RwLock, RwLockUpgradableReadGuard};
-use slotmap::{SecondaryMap, SlotMap};
+use slotmap::{KeyData, SecondaryMap, SlotMap};
 use std::{
     any::{type_name, Any, TypeId},
     fmt::{self, Display},
@@ -24,6 +24,12 @@ slotmap::new_key_type! {
     pub struct EntityId;
 }
 
+impl From<u64> for EntityId {
+    fn from(value: u64) -> Self {
+        Self(KeyData::from_ffi(value))
+    }
+}
+
 impl EntityId {
     pub fn as_u64(self) -> u64 {
         self.0.as_ffi()

crates/gpui/src/color.rs 🔗

@@ -355,16 +355,6 @@ impl Hsla {
     }
 }
 
-// impl From<Hsla> for Rgba {
-//     fn from(value: Hsla) -> Self {
-//         let h = value.h;
-//         let s = value.s;
-//         let l = value.l;
-
-//         let c = (1 - |2L - 1|) X s
-//     }
-// }
-
 impl From<Rgba> for Hsla {
     fn from(color: Rgba) -> Self {
         let r = color.r;

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

@@ -978,12 +978,31 @@ impl Interactivity {
         f: impl FnOnce(&Style, Point<Pixels>, &mut WindowContext),
     ) {
         let style = self.compute_style(Some(bounds), element_state, cx);
+        let z_index = style.z_index.unwrap_or(0);
+
+        let paint_hover_group_handler = |cx: &mut WindowContext| {
+            let hover_group_bounds = self
+                .group_hover_style
+                .as_ref()
+                .and_then(|group_hover| GroupBounds::get(&group_hover.group, cx));
+
+            if let Some(group_bounds) = hover_group_bounds {
+                let hovered = group_bounds.contains(&cx.mouse_position());
+                cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
+                    if phase == DispatchPhase::Capture
+                        && group_bounds.contains(&event.position) != hovered
+                    {
+                        cx.refresh();
+                    }
+                });
+            }
+        };
 
         if style.visibility == Visibility::Hidden {
+            cx.with_z_index(z_index, |cx| paint_hover_group_handler(cx));
             return;
         }
 
-        let z_index = style.z_index.unwrap_or(0);
         cx.with_z_index(z_index, |cx| {
             style.paint(bounds, cx, |cx| {
                 cx.with_text_style(style.text_style().cloned(), |cx| {
@@ -1027,7 +1046,7 @@ impl Interactivity {
                                                 if e.modifiers.command != command_held
                                                     && text_bounds.contains(&cx.mouse_position())
                                                 {
-                                                    cx.notify();
+                                                    cx.refresh();
                                                 }
                                             }
                                         });
@@ -1038,7 +1057,7 @@ impl Interactivity {
                                                 if phase == DispatchPhase::Capture
                                                     && bounds.contains(&event.position) != hovered
                                                 {
-                                                    cx.notify();
+                                                    cx.refresh();
                                                 }
                                             },
                                         );
@@ -1166,21 +1185,7 @@ impl Interactivity {
                             })
                         }
 
-                        let hover_group_bounds = self
-                            .group_hover_style
-                            .as_ref()
-                            .and_then(|group_hover| GroupBounds::get(&group_hover.group, cx));
-
-                        if let Some(group_bounds) = hover_group_bounds {
-                            let hovered = group_bounds.contains(&cx.mouse_position());
-                            cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
-                                if phase == DispatchPhase::Capture
-                                    && group_bounds.contains(&event.position) != hovered
-                                {
-                                    cx.notify();
-                                }
-                            });
-                        }
+                        paint_hover_group_handler(cx);
 
                         if self.hover_style.is_some()
                             || self.base_style.mouse_cursor.is_some()
@@ -1192,7 +1197,7 @@ impl Interactivity {
                                 if phase == DispatchPhase::Capture
                                     && bounds.contains(&event.position) != hovered
                                 {
-                                    cx.notify();
+                                    cx.refresh();
                                 }
                             });
                         }
@@ -1226,7 +1231,7 @@ impl Interactivity {
 
                                                     if can_drop {
                                                         listener(drag.value.as_ref(), cx);
-                                                        cx.notify();
+                                                        cx.refresh();
                                                         cx.stop_propagation();
                                                     }
                                                 }
@@ -1257,7 +1262,7 @@ impl Interactivity {
                                         && interactive_bounds.visibly_contains(&event.position, cx)
                                     {
                                         *pending_mouse_down.borrow_mut() = Some(event.clone());
-                                        cx.notify();
+                                        cx.refresh();
                                     }
                                 }
                             });
@@ -1288,7 +1293,7 @@ impl Interactivity {
                                                     cursor_offset,
                                                 });
                                                 pending_mouse_down.take();
-                                                cx.notify();
+                                                cx.refresh();
                                                 cx.stop_propagation();
                                             }
                                         }
@@ -1308,7 +1313,7 @@ impl Interactivity {
                                             pending_mouse_down.borrow_mut();
                                         if pending_mouse_down.is_some() {
                                             captured_mouse_down = pending_mouse_down.take();
-                                            cx.notify();
+                                            cx.refresh();
                                         }
                                     }
                                     // Fire click handlers during the bubble phase.
@@ -1402,7 +1407,7 @@ impl Interactivity {
                                                         _task: None,
                                                     },
                                                 );
-                                                cx.notify();
+                                                cx.refresh();
                                             })
                                             .ok();
                                         }
@@ -1428,8 +1433,8 @@ impl Interactivity {
                                 .borrow()
                                 .as_ref()
                             {
-                                if active_tooltip.tooltip.is_some() {
-                                    cx.active_tooltip = active_tooltip.tooltip.clone()
+                                if let Some(tooltip) = active_tooltip.tooltip.clone() {
+                                    cx.set_tooltip(tooltip);
                                 }
                             }
                         }
@@ -1442,7 +1447,7 @@ impl Interactivity {
                             cx.on_mouse_event(move |_: &MouseUpEvent, phase, cx| {
                                 if phase == DispatchPhase::Capture {
                                     *active_state.borrow_mut() = ElementClickedState::default();
-                                    cx.notify();
+                                    cx.refresh();
                                 }
                             });
                         } else {
@@ -1460,7 +1465,7 @@ impl Interactivity {
                                     if group || element {
                                         *active_state.borrow_mut() =
                                             ElementClickedState { group, element };
-                                        cx.notify();
+                                        cx.refresh();
                                     }
                                 }
                             });
@@ -1520,7 +1525,7 @@ impl Interactivity {
                                     }
 
                                     if *scroll_offset != old_scroll_offset {
-                                        cx.notify();
+                                        cx.refresh();
                                         cx.stop_propagation();
                                     }
                                 }

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

@@ -109,7 +109,7 @@ impl Element for Img {
                             } else {
                                 cx.spawn(|mut cx| async move {
                                     if image_future.await.ok().is_some() {
-                                        cx.on_next_frame(|cx| cx.notify());
+                                        cx.on_next_frame(|cx| cx.refresh());
                                     }
                                 })
                                 .detach();

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

@@ -43,6 +43,7 @@ pub enum ListAlignment {
 pub struct ListScrollEvent {
     pub visible_range: Range<usize>,
     pub count: usize,
+    pub is_scrolled: bool,
 }
 
 #[derive(Clone)]
@@ -253,12 +254,13 @@ impl StateInner {
                 &ListScrollEvent {
                     visible_range,
                     count: self.items.summary().count,
+                    is_scrolled: self.logical_scroll_top.is_some(),
                 },
                 cx,
             );
         }
 
-        cx.notify();
+        cx.refresh();
     }
 
     fn logical_scroll_top(&self) -> ListOffset {

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

@@ -392,7 +392,7 @@ impl Element for InteractiveText {
                         }
 
                         mouse_down.take();
-                        cx.notify();
+                        cx.refresh();
                     }
                 });
             } else {
@@ -402,7 +402,7 @@ impl Element for InteractiveText {
                             text_state.index_for_position(bounds, event.position)
                         {
                             mouse_down.set(Some(mouse_down_index));
-                            cx.notify();
+                            cx.refresh();
                         }
                     }
                 });

crates/gpui/src/geometry.rs 🔗

@@ -2272,7 +2272,7 @@ impl From<f64> for GlobalPixels {
 /// For example, if the root element's font-size is `16px`, then `1rem` equals `16px`. A length of `2rems` would then be `32px`.
 ///
 /// [set_rem_size]: crate::WindowContext::set_rem_size
-#[derive(Clone, Copy, Default, Add, Sub, Mul, Div, Neg)]
+#[derive(Clone, Copy, Default, Add, Sub, Mul, Div, Neg, PartialEq)]
 pub struct Rems(pub f32);
 
 impl Mul<Pixels> for Rems {
@@ -2295,7 +2295,7 @@ impl Debug for Rems {
 /// affected by the current font size, or a number of rems, which is relative to the font size of
 /// the root element. It is used for specifying dimensions that are either independent of or
 /// related to the typographic scale.
-#[derive(Clone, Copy, Debug, Neg)]
+#[derive(Clone, Copy, Debug, Neg, PartialEq)]
 pub enum AbsoluteLength {
     /// A length in pixels.
     Pixels(Pixels),
@@ -2366,7 +2366,7 @@ impl Default for AbsoluteLength {
 /// This enum represents lengths that have a specific value, as opposed to lengths that are automatically
 /// determined by the context. It includes absolute lengths in pixels or rems, and relative lengths as a
 /// fraction of the parent's size.
-#[derive(Clone, Copy, Neg)]
+#[derive(Clone, Copy, Neg, PartialEq)]
 pub enum DefiniteLength {
     /// An absolute length specified in pixels or rems.
     Absolute(AbsoluteLength),

crates/gpui/src/key_dispatch.rs 🔗

@@ -1,12 +1,13 @@
 use crate::{
-    arena::ArenaRef, Action, ActionRegistry, DispatchPhase, FocusId, KeyBinding, KeyContext,
-    KeyMatch, Keymap, Keystroke, KeystrokeMatcher, WindowContext,
+    Action, ActionRegistry, DispatchPhase, EntityId, FocusId, KeyBinding, KeyContext, KeyMatch,
+    Keymap, Keystroke, KeystrokeMatcher, WindowContext,
 };
-use collections::HashMap;
+use collections::FxHashMap;
 use parking_lot::Mutex;
-use smallvec::SmallVec;
+use smallvec::{smallvec, SmallVec};
 use std::{
     any::{Any, TypeId},
+    mem,
     rc::Rc,
     sync::Arc,
 };
@@ -18,8 +19,9 @@ pub(crate) struct DispatchTree {
     node_stack: Vec<DispatchNodeId>,
     pub(crate) context_stack: Vec<KeyContext>,
     nodes: Vec<DispatchNode>,
-    focusable_node_ids: HashMap<FocusId, DispatchNodeId>,
-    keystroke_matchers: HashMap<SmallVec<[KeyContext; 4]>, KeystrokeMatcher>,
+    focusable_node_ids: FxHashMap<FocusId, DispatchNodeId>,
+    view_node_ids: FxHashMap<EntityId, DispatchNodeId>,
+    keystroke_matchers: FxHashMap<SmallVec<[KeyContext; 4]>, KeystrokeMatcher>,
     keymap: Arc<Mutex<Keymap>>,
     action_registry: Rc<ActionRegistry>,
 }
@@ -30,15 +32,16 @@ pub(crate) struct DispatchNode {
     pub action_listeners: Vec<DispatchActionListener>,
     pub context: Option<KeyContext>,
     focus_id: Option<FocusId>,
+    view_id: Option<EntityId>,
     parent: Option<DispatchNodeId>,
 }
 
-type KeyListener = ArenaRef<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext)>;
+type KeyListener = Rc<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext)>;
 
 #[derive(Clone)]
 pub(crate) struct DispatchActionListener {
     pub(crate) action_type: TypeId,
-    pub(crate) listener: ArenaRef<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext)>,
+    pub(crate) listener: Rc<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext)>,
 }
 
 impl DispatchTree {
@@ -47,8 +50,9 @@ impl DispatchTree {
             node_stack: Vec::new(),
             context_stack: Vec::new(),
             nodes: Vec::new(),
-            focusable_node_ids: HashMap::default(),
-            keystroke_matchers: HashMap::default(),
+            focusable_node_ids: FxHashMap::default(),
+            view_node_ids: FxHashMap::default(),
+            keystroke_matchers: FxHashMap::default(),
             keymap,
             action_registry,
         }
@@ -56,31 +60,101 @@ impl DispatchTree {
 
     pub fn clear(&mut self) {
         self.node_stack.clear();
-        self.nodes.clear();
         self.context_stack.clear();
+        self.nodes.clear();
         self.focusable_node_ids.clear();
+        self.view_node_ids.clear();
         self.keystroke_matchers.clear();
     }
 
-    pub fn push_node(&mut self, context: Option<KeyContext>) {
+    pub fn push_node(
+        &mut self,
+        context: Option<KeyContext>,
+        focus_id: Option<FocusId>,
+        view_id: Option<EntityId>,
+    ) {
         let parent = self.node_stack.last().copied();
         let node_id = DispatchNodeId(self.nodes.len());
         self.nodes.push(DispatchNode {
             parent,
+            focus_id,
+            view_id,
             ..Default::default()
         });
         self.node_stack.push(node_id);
+
         if let Some(context) = context {
             self.active_node().context = Some(context.clone());
             self.context_stack.push(context);
         }
+
+        if let Some(focus_id) = focus_id {
+            self.focusable_node_ids.insert(focus_id, node_id);
+        }
+
+        if let Some(view_id) = view_id {
+            self.view_node_ids.insert(view_id, node_id);
+        }
     }
 
     pub fn pop_node(&mut self) {
-        let node_id = self.node_stack.pop().unwrap();
-        if self.nodes[node_id.0].context.is_some() {
+        let node = &self.nodes[self.active_node_id().0];
+        if node.context.is_some() {
             self.context_stack.pop();
         }
+        self.node_stack.pop();
+    }
+
+    fn move_node(&mut self, source: &mut DispatchNode) {
+        self.push_node(source.context.take(), source.focus_id, source.view_id);
+        let target = self.active_node();
+        target.key_listeners = mem::take(&mut source.key_listeners);
+        target.action_listeners = mem::take(&mut source.action_listeners);
+    }
+
+    pub fn reuse_view(&mut self, view_id: EntityId, source: &mut Self) -> SmallVec<[EntityId; 8]> {
+        let view_source_node_id = source
+            .view_node_ids
+            .get(&view_id)
+            .expect("view should exist in previous dispatch tree");
+        let view_source_node = &mut source.nodes[view_source_node_id.0];
+        self.move_node(view_source_node);
+
+        let mut grafted_view_ids = smallvec![view_id];
+        let mut source_stack = vec![*view_source_node_id];
+        for (source_node_id, source_node) in source
+            .nodes
+            .iter_mut()
+            .enumerate()
+            .skip(view_source_node_id.0 + 1)
+        {
+            let source_node_id = DispatchNodeId(source_node_id);
+            while let Some(source_ancestor) = source_stack.last() {
+                if source_node.parent != Some(*source_ancestor) {
+                    source_stack.pop();
+                    self.pop_node();
+                } else {
+                    break;
+                }
+            }
+
+            if source_stack.is_empty() {
+                break;
+            } else {
+                source_stack.push(source_node_id);
+                self.move_node(source_node);
+                if let Some(view_id) = source_node.view_id {
+                    grafted_view_ids.push(view_id);
+                }
+            }
+        }
+
+        while !source_stack.is_empty() {
+            source_stack.pop();
+            self.pop_node();
+        }
+
+        grafted_view_ids
     }
 
     pub fn clear_pending_keystrokes(&mut self) {
@@ -117,7 +191,7 @@ impl DispatchTree {
     pub fn on_action(
         &mut self,
         action_type: TypeId,
-        listener: ArenaRef<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext)>,
+        listener: Rc<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext)>,
     ) {
         self.active_node()
             .action_listeners
@@ -127,12 +201,6 @@ impl DispatchTree {
             });
     }
 
-    pub fn make_focusable(&mut self, focus_id: FocusId) {
-        let node_id = self.active_node_id();
-        self.active_node().focus_id = Some(focus_id);
-        self.focusable_node_ids.insert(focus_id, node_id);
-    }
-
     pub fn focus_contains(&self, parent: FocusId, child: FocusId) -> bool {
         if parent == child {
             return true;
@@ -261,6 +329,20 @@ impl DispatchTree {
         focus_path
     }
 
+    pub fn view_path(&self, view_id: EntityId) -> SmallVec<[EntityId; 8]> {
+        let mut view_path: SmallVec<[EntityId; 8]> = SmallVec::new();
+        let mut current_node_id = self.view_node_ids.get(&view_id).copied();
+        while let Some(node_id) = current_node_id {
+            let node = self.node(node_id);
+            if let Some(view_id) = node.view_id {
+                view_path.push(view_id);
+            }
+            current_node_id = node.parent;
+        }
+        view_path.reverse(); // Reverse the path so it goes from the root to the view node.
+        view_path
+    }
+
     pub fn node(&self, node_id: DispatchNodeId) -> &DispatchNode {
         &self.nodes[node_id.0]
     }

crates/gpui/src/platform.rs 🔗

@@ -44,8 +44,6 @@ pub(crate) fn current_platform() -> Rc<dyn Platform> {
     Rc::new(MacPlatform::new())
 }
 
-pub type DrawWindow = Box<dyn FnMut() -> Result<Scene>>;
-
 pub(crate) trait Platform: 'static {
     fn background_executor(&self) -> BackgroundExecutor;
     fn foreground_executor(&self) -> ForegroundExecutor;
@@ -66,7 +64,6 @@ pub(crate) trait Platform: 'static {
         &self,
         handle: AnyWindowHandle,
         options: WindowOptions,
-        draw: DrawWindow,
     ) -> Box<dyn PlatformWindow>;
 
     fn set_display_link_output_callback(
@@ -148,7 +145,7 @@ pub trait PlatformWindow {
     fn modifiers(&self) -> Modifiers;
     fn as_any_mut(&mut self) -> &mut dyn Any;
     fn set_input_handler(&mut self, input_handler: Box<dyn PlatformInputHandler>);
-    fn clear_input_handler(&mut self);
+    fn take_input_handler(&mut self) -> Option<Box<dyn PlatformInputHandler>>;
     fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str]) -> oneshot::Receiver<usize>;
     fn activate(&self);
     fn set_title(&mut self, title: &str);
@@ -157,6 +154,7 @@ pub trait PlatformWindow {
     fn minimize(&self);
     fn zoom(&self);
     fn toggle_full_screen(&self);
+    fn on_request_frame(&self, callback: Box<dyn FnMut()>);
     fn on_input(&self, callback: Box<dyn FnMut(InputEvent) -> bool>);
     fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>);
     fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>);
@@ -167,6 +165,7 @@ pub trait PlatformWindow {
     fn on_appearance_changed(&self, callback: Box<dyn FnMut()>);
     fn is_topmost_for_position(&self, position: Point<Pixels>) -> bool;
     fn invalidate(&self);
+    fn draw(&self, scene: &Scene);
 
     fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
 

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

@@ -1,10 +1,16 @@
 use crate::{point, size, Bounds, DisplayId, GlobalPixels, PlatformDisplay};
 use anyhow::Result;
+use cocoa::{
+    appkit::NSScreen,
+    base::{id, nil},
+    foundation::{NSDictionary, NSString},
+};
 use core_foundation::uuid::{CFUUIDGetUUIDBytes, CFUUIDRef};
 use core_graphics::{
     display::{CGDirectDisplayID, CGDisplayBounds, CGGetActiveDisplayList},
     geometry::{CGPoint, CGRect, CGSize},
 };
+use objc::{msg_send, sel, sel_impl};
 use std::any::Any;
 use uuid::Uuid;
 
@@ -27,23 +33,41 @@ impl MacDisplay {
     /// Get the primary screen - the one with the menu bar, and whose bottom left
     /// corner is at the origin of the AppKit coordinate system.
     pub fn primary() -> Self {
-        Self::all().next().unwrap()
+        // Instead of iterating through all active systems displays via `all()` we use the first
+        // NSScreen and gets its CGDirectDisplayID, because we can't be sure that `CGGetActiveDisplayList`
+        // will always return a list of active displays (machine might be sleeping).
+        //
+        // The following is what Chromium does too:
+        //
+        // https://chromium.googlesource.com/chromium/src/+/66.0.3359.158/ui/display/mac/screen_mac.mm#56
+        unsafe {
+            let screens = NSScreen::screens(nil);
+            let screen = cocoa::foundation::NSArray::objectAtIndex(screens, 0);
+            let device_description = NSScreen::deviceDescription(screen);
+            let screen_number_key: id = NSString::alloc(nil).init_str("NSScreenNumber");
+            let screen_number = device_description.objectForKey_(screen_number_key);
+            let screen_number: CGDirectDisplayID = msg_send![screen_number, unsignedIntegerValue];
+            Self(screen_number)
+        }
     }
 
     /// Obtains an iterator over all currently active system displays.
     pub fn all() -> impl Iterator<Item = Self> {
         unsafe {
-            let mut display_count: u32 = 0;
-            let result = CGGetActiveDisplayList(0, std::ptr::null_mut(), &mut display_count);
+            // We're assuming there aren't more than 32 displays connected to the system.
+            let mut displays = Vec::with_capacity(32);
+            let mut display_count = 0;
+            let result = CGGetActiveDisplayList(
+                displays.capacity() as u32,
+                displays.as_mut_ptr(),
+                &mut display_count,
+            );
 
             if result == 0 {
-                let mut displays = Vec::with_capacity(display_count as usize);
-                CGGetActiveDisplayList(display_count, displays.as_mut_ptr(), &mut display_count);
                 displays.set_len(display_count as usize);
-
                 displays.into_iter().map(MacDisplay)
             } else {
-                panic!("Failed to get active display list");
+                panic!("Failed to get active display list. Result: {result}");
             }
         }
     }

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

@@ -18,7 +18,7 @@ use smallvec::SmallVec;
 use std::{ffi::c_void, mem, ptr, sync::Arc};
 
 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.
+const INSTANCE_BUFFER_SIZE: usize = 32 * 1024 * 1024; // This is an arbitrary decision. There's probably a more optimal value (maybe even we could adjust dynamically...)
 
 pub(crate) struct MetalRenderer {
     layer: metal::MetalLayer,
@@ -204,7 +204,11 @@ impl MetalRenderer {
         let command_buffer = command_queue.new_command_buffer();
         let mut instance_offset = 0;
 
-        let path_tiles = self.rasterize_paths(scene.paths(), &mut instance_offset, command_buffer);
+        let Some(path_tiles) =
+            self.rasterize_paths(scene.paths(), &mut instance_offset, command_buffer)
+        else {
+            panic!("failed to rasterize {} paths", scene.paths().len());
+        };
 
         let render_pass_descriptor = metal::RenderPassDescriptor::new();
         let color_attachment = render_pass_descriptor
@@ -228,67 +232,67 @@ impl MetalRenderer {
             zfar: 1.0,
         });
         for batch in scene.batches() {
-            match batch {
-                PrimitiveBatch::Shadows(shadows) => {
-                    self.draw_shadows(
-                        shadows,
-                        &mut instance_offset,
-                        viewport_size,
-                        command_encoder,
-                    );
-                }
+            let ok = match batch {
+                PrimitiveBatch::Shadows(shadows) => self.draw_shadows(
+                    shadows,
+                    &mut instance_offset,
+                    viewport_size,
+                    command_encoder,
+                ),
                 PrimitiveBatch::Quads(quads) => {
-                    self.draw_quads(quads, &mut instance_offset, viewport_size, command_encoder);
-                }
-                PrimitiveBatch::Paths(paths) => {
-                    self.draw_paths(
-                        paths,
-                        &path_tiles,
-                        &mut instance_offset,
-                        viewport_size,
-                        command_encoder,
-                    );
-                }
-                PrimitiveBatch::Underlines(underlines) => {
-                    self.draw_underlines(
-                        underlines,
-                        &mut instance_offset,
-                        viewport_size,
-                        command_encoder,
-                    );
+                    self.draw_quads(quads, &mut instance_offset, viewport_size, command_encoder)
                 }
+                PrimitiveBatch::Paths(paths) => self.draw_paths(
+                    paths,
+                    &path_tiles,
+                    &mut instance_offset,
+                    viewport_size,
+                    command_encoder,
+                ),
+                PrimitiveBatch::Underlines(underlines) => self.draw_underlines(
+                    underlines,
+                    &mut instance_offset,
+                    viewport_size,
+                    command_encoder,
+                ),
                 PrimitiveBatch::MonochromeSprites {
                     texture_id,
                     sprites,
-                } => {
-                    self.draw_monochrome_sprites(
-                        texture_id,
-                        sprites,
-                        &mut instance_offset,
-                        viewport_size,
-                        command_encoder,
-                    );
-                }
+                } => self.draw_monochrome_sprites(
+                    texture_id,
+                    sprites,
+                    &mut instance_offset,
+                    viewport_size,
+                    command_encoder,
+                ),
                 PrimitiveBatch::PolychromeSprites {
                     texture_id,
                     sprites,
-                } => {
-                    self.draw_polychrome_sprites(
-                        texture_id,
-                        sprites,
-                        &mut instance_offset,
-                        viewport_size,
-                        command_encoder,
-                    );
-                }
-                PrimitiveBatch::Surfaces(surfaces) => {
-                    self.draw_surfaces(
-                        surfaces,
-                        &mut instance_offset,
-                        viewport_size,
-                        command_encoder,
-                    );
-                }
+                } => self.draw_polychrome_sprites(
+                    texture_id,
+                    sprites,
+                    &mut instance_offset,
+                    viewport_size,
+                    command_encoder,
+                ),
+                PrimitiveBatch::Surfaces(surfaces) => self.draw_surfaces(
+                    surfaces,
+                    &mut instance_offset,
+                    viewport_size,
+                    command_encoder,
+                ),
+            };
+
+            if !ok {
+                panic!("scene too large: {} paths, {} shadows, {} quads, {} underlines, {} mono, {} poly, {} surfaces",
+                    scene.paths.len(),
+                    scene.shadows.len(),
+                    scene.quads.len(),
+                    scene.underlines.len(),
+                    scene.monochrome_sprites.len(),
+                    scene.polychrome_sprites.len(),
+                    scene.surfaces.len(),
+                )
             }
         }
 
@@ -311,7 +315,7 @@ impl MetalRenderer {
         paths: &[Path<ScaledPixels>],
         offset: &mut usize,
         command_buffer: &metal::CommandBufferRef,
-    ) -> HashMap<PathId, AtlasTile> {
+    ) -> Option<HashMap<PathId, AtlasTile>> {
         let mut tiles = HashMap::default();
         let mut vertices_by_texture_id = HashMap::default();
         for path in paths {
@@ -337,10 +341,9 @@ impl MetalRenderer {
         for (texture_id, vertices) in vertices_by_texture_id {
             align_offset(offset);
             let next_offset = *offset + vertices.len() * mem::size_of::<PathVertex<ScaledPixels>>();
-            assert!(
-                next_offset <= INSTANCE_BUFFER_SIZE,
-                "instance buffer exhausted"
-            );
+            if next_offset > INSTANCE_BUFFER_SIZE {
+                return None;
+            }
 
             let render_pass_descriptor = metal::RenderPassDescriptor::new();
             let color_attachment = render_pass_descriptor
@@ -389,7 +392,7 @@ impl MetalRenderer {
             *offset = next_offset;
         }
 
-        tiles
+        Some(tiles)
     }
 
     fn draw_shadows(
@@ -398,9 +401,9 @@ impl MetalRenderer {
         offset: &mut usize,
         viewport_size: Size<DevicePixels>,
         command_encoder: &metal::RenderCommandEncoderRef,
-    ) {
+    ) -> bool {
         if shadows.is_empty() {
-            return;
+            return true;
         }
         align_offset(offset);
 
@@ -429,6 +432,12 @@ impl MetalRenderer {
 
         let shadow_bytes_len = std::mem::size_of_val(shadows);
         let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
+
+        let next_offset = *offset + shadow_bytes_len;
+        if next_offset > INSTANCE_BUFFER_SIZE {
+            return false;
+        }
+
         unsafe {
             ptr::copy_nonoverlapping(
                 shadows.as_ptr() as *const u8,
@@ -437,12 +446,6 @@ impl MetalRenderer {
             );
         }
 
-        let next_offset = *offset + shadow_bytes_len;
-        assert!(
-            next_offset <= INSTANCE_BUFFER_SIZE,
-            "instance buffer exhausted"
-        );
-
         command_encoder.draw_primitives_instanced(
             metal::MTLPrimitiveType::Triangle,
             0,
@@ -450,6 +453,7 @@ impl MetalRenderer {
             shadows.len() as u64,
         );
         *offset = next_offset;
+        true
     }
 
     fn draw_quads(
@@ -458,9 +462,9 @@ impl MetalRenderer {
         offset: &mut usize,
         viewport_size: Size<DevicePixels>,
         command_encoder: &metal::RenderCommandEncoderRef,
-    ) {
+    ) -> bool {
         if quads.is_empty() {
-            return;
+            return true;
         }
         align_offset(offset);
 
@@ -489,16 +493,16 @@ impl MetalRenderer {
 
         let quad_bytes_len = std::mem::size_of_val(quads);
         let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
+
+        let next_offset = *offset + quad_bytes_len;
+        if next_offset > INSTANCE_BUFFER_SIZE {
+            return false;
+        }
+
         unsafe {
             ptr::copy_nonoverlapping(quads.as_ptr() as *const u8, buffer_contents, quad_bytes_len);
         }
 
-        let next_offset = *offset + quad_bytes_len;
-        assert!(
-            next_offset <= INSTANCE_BUFFER_SIZE,
-            "instance buffer exhausted"
-        );
-
         command_encoder.draw_primitives_instanced(
             metal::MTLPrimitiveType::Triangle,
             0,
@@ -506,6 +510,7 @@ impl MetalRenderer {
             quads.len() as u64,
         );
         *offset = next_offset;
+        true
     }
 
     fn draw_paths(
@@ -515,9 +520,9 @@ impl MetalRenderer {
         offset: &mut usize,
         viewport_size: Size<DevicePixels>,
         command_encoder: &metal::RenderCommandEncoderRef,
-    ) {
+    ) -> bool {
         if paths.is_empty() {
-            return;
+            return true;
         }
 
         command_encoder.set_render_pipeline_state(&self.path_sprites_pipeline_state);
@@ -587,8 +592,14 @@ impl MetalRenderer {
                     .set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture));
 
                 let sprite_bytes_len = mem::size_of::<MonochromeSprite>() * sprites.len();
+                let next_offset = *offset + sprite_bytes_len;
+                if next_offset > INSTANCE_BUFFER_SIZE {
+                    return false;
+                }
+
                 let buffer_contents =
                     unsafe { (self.instances.contents() as *mut u8).add(*offset) };
+
                 unsafe {
                     ptr::copy_nonoverlapping(
                         sprites.as_ptr() as *const u8,
@@ -597,12 +608,6 @@ impl MetalRenderer {
                     );
                 }
 
-                let next_offset = *offset + sprite_bytes_len;
-                assert!(
-                    next_offset <= INSTANCE_BUFFER_SIZE,
-                    "instance buffer exhausted"
-                );
-
                 command_encoder.draw_primitives_instanced(
                     metal::MTLPrimitiveType::Triangle,
                     0,
@@ -613,6 +618,7 @@ impl MetalRenderer {
                 sprites.clear();
             }
         }
+        true
     }
 
     fn draw_underlines(
@@ -621,9 +627,9 @@ impl MetalRenderer {
         offset: &mut usize,
         viewport_size: Size<DevicePixels>,
         command_encoder: &metal::RenderCommandEncoderRef,
-    ) {
+    ) -> bool {
         if underlines.is_empty() {
-            return;
+            return true;
         }
         align_offset(offset);
 
@@ -661,10 +667,9 @@ impl MetalRenderer {
         }
 
         let next_offset = *offset + quad_bytes_len;
-        assert!(
-            next_offset <= INSTANCE_BUFFER_SIZE,
-            "instance buffer exhausted"
-        );
+        if next_offset > INSTANCE_BUFFER_SIZE {
+            return false;
+        }
 
         command_encoder.draw_primitives_instanced(
             metal::MTLPrimitiveType::Triangle,
@@ -673,6 +678,7 @@ impl MetalRenderer {
             underlines.len() as u64,
         );
         *offset = next_offset;
+        true
     }
 
     fn draw_monochrome_sprites(
@@ -682,9 +688,9 @@ impl MetalRenderer {
         offset: &mut usize,
         viewport_size: Size<DevicePixels>,
         command_encoder: &metal::RenderCommandEncoderRef,
-    ) {
+    ) -> bool {
         if sprites.is_empty() {
-            return;
+            return true;
         }
         align_offset(offset);
 
@@ -723,6 +729,12 @@ impl MetalRenderer {
 
         let sprite_bytes_len = std::mem::size_of_val(sprites);
         let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
+
+        let next_offset = *offset + sprite_bytes_len;
+        if next_offset > INSTANCE_BUFFER_SIZE {
+            return false;
+        }
+
         unsafe {
             ptr::copy_nonoverlapping(
                 sprites.as_ptr() as *const u8,
@@ -731,12 +743,6 @@ impl MetalRenderer {
             );
         }
 
-        let next_offset = *offset + sprite_bytes_len;
-        assert!(
-            next_offset <= INSTANCE_BUFFER_SIZE,
-            "instance buffer exhausted"
-        );
-
         command_encoder.draw_primitives_instanced(
             metal::MTLPrimitiveType::Triangle,
             0,
@@ -744,6 +750,7 @@ impl MetalRenderer {
             sprites.len() as u64,
         );
         *offset = next_offset;
+        true
     }
 
     fn draw_polychrome_sprites(
@@ -753,9 +760,9 @@ impl MetalRenderer {
         offset: &mut usize,
         viewport_size: Size<DevicePixels>,
         command_encoder: &metal::RenderCommandEncoderRef,
-    ) {
+    ) -> bool {
         if sprites.is_empty() {
-            return;
+            return true;
         }
         align_offset(offset);
 
@@ -794,6 +801,12 @@ impl MetalRenderer {
 
         let sprite_bytes_len = std::mem::size_of_val(sprites);
         let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
+
+        let next_offset = *offset + sprite_bytes_len;
+        if next_offset > INSTANCE_BUFFER_SIZE {
+            return false;
+        }
+
         unsafe {
             ptr::copy_nonoverlapping(
                 sprites.as_ptr() as *const u8,
@@ -802,12 +815,6 @@ impl MetalRenderer {
             );
         }
 
-        let next_offset = *offset + sprite_bytes_len;
-        assert!(
-            next_offset <= INSTANCE_BUFFER_SIZE,
-            "instance buffer exhausted"
-        );
-
         command_encoder.draw_primitives_instanced(
             metal::MTLPrimitiveType::Triangle,
             0,
@@ -815,6 +822,7 @@ impl MetalRenderer {
             sprites.len() as u64,
         );
         *offset = next_offset;
+        true
     }
 
     fn draw_surfaces(
@@ -823,7 +831,7 @@ impl MetalRenderer {
         offset: &mut usize,
         viewport_size: Size<DevicePixels>,
         command_encoder: &metal::RenderCommandEncoderRef,
-    ) {
+    ) -> bool {
         command_encoder.set_render_pipeline_state(&self.surfaces_pipeline_state);
         command_encoder.set_vertex_buffer(
             SurfaceInputIndex::Vertices as u64,
@@ -874,10 +882,9 @@ impl MetalRenderer {
 
             align_offset(offset);
             let next_offset = *offset + mem::size_of::<Surface>();
-            assert!(
-                next_offset <= INSTANCE_BUFFER_SIZE,
-                "instance buffer exhausted"
-            );
+            if next_offset > INSTANCE_BUFFER_SIZE {
+                return false;
+            }
 
             command_encoder.set_vertex_buffer(
                 SurfaceInputIndex::Surfaces as u64,
@@ -913,6 +920,7 @@ impl MetalRenderer {
             command_encoder.draw_primitives(metal::MTLPrimitiveType::Triangle, 0, 6);
             *offset = next_offset;
         }
+        true
     }
 }
 

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

@@ -378,7 +378,7 @@ fn toggle_open_type_feature(
                 new_descriptor.as_concrete_TypeRef(),
             );
             let new_font = CTFont::wrap_under_create_rule(new_font);
-            *font = Font::from_native_font(new_font);
+            *font = Font::from_native_font(&new_font);
         }
     }
 }

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

@@ -3,8 +3,7 @@ use crate::{
     Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId,
     ForegroundExecutor, InputEvent, Keymap, MacDispatcher, MacDisplay, MacDisplayLinker,
     MacTextSystem, MacWindow, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay,
-    PlatformTextSystem, PlatformWindow, Result, Scene, SemanticVersion, VideoTimestamp,
-    WindowOptions,
+    PlatformTextSystem, PlatformWindow, Result, SemanticVersion, VideoTimestamp, WindowOptions,
 };
 use anyhow::anyhow;
 use block::ConcreteBlock;
@@ -498,14 +497,8 @@ impl Platform for MacPlatform {
         &self,
         handle: AnyWindowHandle,
         options: WindowOptions,
-        draw: Box<dyn FnMut() -> Result<Scene>>,
     ) -> Box<dyn PlatformWindow> {
-        Box::new(MacWindow::open(
-            handle,
-            options,
-            draw,
-            self.foreground_executor(),
-        ))
+        Box::new(MacWindow::open(handle, options, self.foreground_executor()))
     }
 
     fn set_display_link_output_callback(
@@ -985,8 +978,12 @@ extern "C" fn send_event(this: &mut Object, _sel: Sel, native_event: id) {
     unsafe {
         if let Some(event) = InputEvent::from_native(native_event, None) {
             let platform = get_mac_platform(this);
-            if let Some(callback) = platform.0.lock().event.as_mut() {
-                if !callback(event) {
+            let mut lock = platform.0.lock();
+            if let Some(mut callback) = lock.event.take() {
+                drop(lock);
+                let result = callback(event);
+                platform.0.lock().event.get_or_insert(callback);
+                if !result {
                     return;
                 }
             }
@@ -1011,30 +1008,42 @@ extern "C" fn did_finish_launching(this: &mut Object, _: Sel, _: id) {
 extern "C" fn should_handle_reopen(this: &mut Object, _: Sel, _: id, has_open_windows: bool) {
     if !has_open_windows {
         let platform = unsafe { get_mac_platform(this) };
-        if let Some(callback) = platform.0.lock().reopen.as_mut() {
+        let mut lock = platform.0.lock();
+        if let Some(mut callback) = lock.reopen.take() {
+            drop(lock);
             callback();
+            platform.0.lock().reopen.get_or_insert(callback);
         }
     }
 }
 
 extern "C" fn did_become_active(this: &mut Object, _: Sel, _: id) {
     let platform = unsafe { get_mac_platform(this) };
-    if let Some(callback) = platform.0.lock().become_active.as_mut() {
+    let mut lock = platform.0.lock();
+    if let Some(mut callback) = lock.become_active.take() {
+        drop(lock);
         callback();
+        platform.0.lock().become_active.get_or_insert(callback);
     }
 }
 
 extern "C" fn did_resign_active(this: &mut Object, _: Sel, _: id) {
     let platform = unsafe { get_mac_platform(this) };
-    if let Some(callback) = platform.0.lock().resign_active.as_mut() {
+    let mut lock = platform.0.lock();
+    if let Some(mut callback) = lock.resign_active.take() {
+        drop(lock);
         callback();
+        platform.0.lock().resign_active.get_or_insert(callback);
     }
 }
 
 extern "C" fn will_terminate(this: &mut Object, _: Sel, _: id) {
     let platform = unsafe { get_mac_platform(this) };
-    if let Some(callback) = platform.0.lock().quit.as_mut() {
+    let mut lock = platform.0.lock();
+    if let Some(mut callback) = lock.quit.take() {
+        drop(lock);
         callback();
+        platform.0.lock().quit.get_or_insert(callback);
     }
 }
 
@@ -1054,22 +1063,27 @@ extern "C" fn open_urls(this: &mut Object, _: Sel, _: id, urls: id) {
             .collect::<Vec<_>>()
     };
     let platform = unsafe { get_mac_platform(this) };
-    if let Some(callback) = platform.0.lock().open_urls.as_mut() {
+    let mut lock = platform.0.lock();
+    if let Some(mut callback) = lock.open_urls.take() {
+        drop(lock);
         callback(urls);
+        platform.0.lock().open_urls.get_or_insert(callback);
     }
 }
 
 extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) {
     unsafe {
         let platform = get_mac_platform(this);
-        let mut platform = platform.0.lock();
-        if let Some(mut callback) = platform.menu_command.take() {
+        let mut lock = platform.0.lock();
+        if let Some(mut callback) = lock.menu_command.take() {
             let tag: NSInteger = msg_send![item, tag];
             let index = tag as usize;
-            if let Some(action) = platform.menu_actions.get(index) {
-                callback(action.as_ref());
+            if let Some(action) = lock.menu_actions.get(index) {
+                let action = action.boxed_clone();
+                drop(lock);
+                callback(&*action);
             }
-            platform.menu_command = Some(callback);
+            platform.0.lock().menu_command.get_or_insert(callback);
         }
     }
 }
@@ -1078,14 +1092,20 @@ extern "C" fn validate_menu_item(this: &mut Object, _: Sel, item: id) -> bool {
     unsafe {
         let mut result = false;
         let platform = get_mac_platform(this);
-        let mut platform = platform.0.lock();
-        if let Some(mut callback) = platform.validate_menu_command.take() {
+        let mut lock = platform.0.lock();
+        if let Some(mut callback) = lock.validate_menu_command.take() {
             let tag: NSInteger = msg_send![item, tag];
             let index = tag as usize;
-            if let Some(action) = platform.menu_actions.get(index) {
+            if let Some(action) = lock.menu_actions.get(index) {
+                let action = action.boxed_clone();
+                drop(lock);
                 result = callback(action.as_ref());
             }
-            platform.validate_menu_command = Some(callback);
+            platform
+                .0
+                .lock()
+                .validate_menu_command
+                .get_or_insert(callback);
         }
         result
     }
@@ -1094,10 +1114,11 @@ extern "C" fn validate_menu_item(this: &mut Object, _: Sel, item: id) -> bool {
 extern "C" fn menu_will_open(this: &mut Object, _: Sel, _: id) {
     unsafe {
         let platform = get_mac_platform(this);
-        let mut platform = platform.0.lock();
-        if let Some(mut callback) = platform.will_open_menu.take() {
+        let mut lock = platform.0.lock();
+        if let Some(mut callback) = lock.will_open_menu.take() {
+            drop(lock);
             callback();
-            platform.will_open_menu = Some(callback);
+            platform.0.lock().will_open_menu.get_or_insert(callback);
         }
     }
 }

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

@@ -190,6 +190,9 @@ impl MacTextSystemState {
         for font in family.fonts() {
             let mut font = font.load()?;
             open_type::apply_features(&mut font, features);
+            let Some(_) = font.glyph_for_char('m') else {
+                continue;
+            };
             let font_id = FontId(self.fonts.len());
             font_ids.push(font_id);
             let postscript_name = font.postscript_name().unwrap();

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

@@ -1,6 +1,6 @@
 use super::{display_bounds_from_native, ns_string, MacDisplay, MetalRenderer, NSRange};
 use crate::{
-    display_bounds_to_native, point, px, size, AnyWindowHandle, Bounds, DrawWindow, ExternalPaths,
+    display_bounds_to_native, point, px, size, AnyWindowHandle, Bounds, ExternalPaths,
     FileDropEvent, ForegroundExecutor, GlobalPixels, InputEvent, KeyDownEvent, Keystroke,
     Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
     Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point,
@@ -46,7 +46,6 @@ use std::{
     sync::{Arc, Weak},
     time::Duration,
 };
-use util::ResultExt;
 
 const WINDOW_STATE_IVAR: &str = "windowState";
 
@@ -318,8 +317,8 @@ struct MacWindowState {
     executor: ForegroundExecutor,
     native_window: id,
     renderer: MetalRenderer,
-    draw: Option<DrawWindow>,
     kind: WindowKind,
+    request_frame_callback: Option<Box<dyn FnMut()>>,
     event_callback: Option<Box<dyn FnMut(InputEvent) -> bool>>,
     activate_callback: Option<Box<dyn FnMut(bool)>>,
     resize_callback: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
@@ -455,7 +454,6 @@ impl MacWindow {
     pub fn open(
         handle: AnyWindowHandle,
         options: WindowOptions,
-        draw: DrawWindow,
         executor: ForegroundExecutor,
     ) -> Self {
         unsafe {
@@ -486,7 +484,7 @@ impl MacWindow {
 
             let display = options
                 .display_id
-                .and_then(|display_id| MacDisplay::all().find(|display| display.id() == display_id))
+                .and_then(MacDisplay::find_by_id)
                 .unwrap_or_else(MacDisplay::primary);
 
             let mut target_screen = nil;
@@ -547,8 +545,8 @@ impl MacWindow {
                 executor,
                 native_window,
                 renderer: MetalRenderer::new(true),
-                draw: Some(draw),
                 kind: options.kind,
+                request_frame_callback: None,
                 event_callback: None,
                 activate_callback: None,
                 resize_callback: None,
@@ -770,8 +768,8 @@ impl PlatformWindow for MacWindow {
         self.0.as_ref().lock().input_handler = Some(input_handler);
     }
 
-    fn clear_input_handler(&mut self) {
-        self.0.as_ref().lock().input_handler = None;
+    fn take_input_handler(&mut self) -> Option<Box<dyn PlatformInputHandler>> {
+        self.0.as_ref().lock().input_handler.take()
     }
 
     fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str]) -> oneshot::Receiver<usize> {
@@ -926,6 +924,10 @@ impl PlatformWindow for MacWindow {
             .detach();
     }
 
+    fn on_request_frame(&self, callback: Box<dyn FnMut()>) {
+        self.0.as_ref().lock().request_frame_callback = Some(callback);
+    }
+
     fn on_input(&self, callback: Box<dyn FnMut(InputEvent) -> bool>) {
         self.0.as_ref().lock().event_callback = Some(callback);
     }
@@ -990,6 +992,11 @@ impl PlatformWindow for MacWindow {
         }
     }
 
+    fn draw(&self, scene: &crate::Scene) {
+        let mut this = self.0.lock();
+        this.renderer.draw(scene);
+    }
+
     fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
         self.0.lock().renderer.sprite_atlas().clone()
     }
@@ -1437,15 +1444,12 @@ extern "C" fn set_frame_size(this: &Object, _: Sel, size: NSSize) {
 }
 
 extern "C" fn display_layer(this: &Object, _: Sel, _: id) {
-    unsafe {
-        let window_state = get_window_state(this);
-        let mut draw = window_state.lock().draw.take().unwrap();
-        let scene = draw().log_err();
-        let mut window_state = window_state.lock();
-        window_state.draw = Some(draw);
-        if let Some(scene) = scene {
-            window_state.renderer.draw(&scene);
-        }
+    let window_state = unsafe { get_window_state(this) };
+    let mut lock = window_state.lock();
+    if let Some(mut callback) = lock.request_frame_callback.take() {
+        drop(lock);
+        callback();
+        window_state.lock().request_frame_callback = Some(callback);
     }
 }
 

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

@@ -1,7 +1,6 @@
 use crate::{
     AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, ForegroundExecutor,
-    Keymap, Platform, PlatformDisplay, PlatformTextSystem, Scene, TestDisplay, TestWindow,
-    WindowOptions,
+    Keymap, Platform, PlatformDisplay, PlatformTextSystem, TestDisplay, TestWindow, WindowOptions,
 };
 use anyhow::{anyhow, Result};
 use collections::VecDeque;
@@ -166,7 +165,6 @@ impl Platform for TestPlatform {
         &self,
         handle: AnyWindowHandle,
         options: WindowOptions,
-        _draw: Box<dyn FnMut() -> Result<Scene>>,
     ) -> Box<dyn crate::PlatformWindow> {
         let window = TestWindow::new(
             options,

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

@@ -167,8 +167,8 @@ impl PlatformWindow for TestWindow {
         self.0.lock().input_handler = Some(input_handler);
     }
 
-    fn clear_input_handler(&mut self) {
-        self.0.lock().input_handler = None;
+    fn take_input_handler(&mut self) -> Option<Box<dyn PlatformInputHandler>> {
+        self.0.lock().input_handler.take()
     }
 
     fn prompt(
@@ -218,6 +218,8 @@ impl PlatformWindow for TestWindow {
         unimplemented!()
     }
 
+    fn on_request_frame(&self, _callback: Box<dyn FnMut()>) {}
+
     fn on_input(&self, callback: Box<dyn FnMut(crate::InputEvent) -> bool>) {
         self.0.lock().input_callback = Some(callback)
     }
@@ -254,9 +256,9 @@ impl PlatformWindow for TestWindow {
         unimplemented!()
     }
 
-    fn invalidate(&self) {
-        // (self.draw.lock())().unwrap();
-    }
+    fn invalidate(&self) {}
+
+    fn draw(&self, _scene: &crate::Scene) {}
 
     fn sprite_atlas(&self) -> sync::Arc<dyn crate::PlatformAtlas> {
         self.0.lock().sprite_atlas.clone()

crates/gpui/src/scene.rs 🔗

@@ -1,9 +1,9 @@
 use crate::{
-    point, AtlasTextureId, AtlasTile, Bounds, ContentMask, Corners, Edges, Hsla, Pixels, Point,
-    ScaledPixels, StackingOrder,
+    point, AtlasTextureId, AtlasTile, Bounds, ContentMask, Corners, Edges, EntityId, Hsla, Pixels,
+    Point, ScaledPixels, StackingOrder,
 };
-use collections::BTreeMap;
-use std::{fmt::Debug, iter::Peekable, mem, slice};
+use collections::{BTreeMap, FxHashSet};
+use std::{fmt::Debug, iter::Peekable, slice};
 
 // Exported to metal
 pub(crate) type PointF = Point<f32>;
@@ -11,74 +11,85 @@ pub(crate) type PointF = Point<f32>;
 pub(crate) type PathVertex_ScaledPixels = PathVertex<ScaledPixels>;
 
 pub type LayerId = u32;
-
 pub type DrawOrder = u32;
 
-#[derive(Default)]
-pub(crate) struct SceneBuilder {
-    last_order: Option<(StackingOrder, LayerId)>,
-    layers_by_order: BTreeMap<StackingOrder, LayerId>,
-    shadows: Vec<Shadow>,
-    quads: Vec<Quad>,
-    paths: Vec<Path<ScaledPixels>>,
-    underlines: Vec<Underline>,
-    monochrome_sprites: Vec<MonochromeSprite>,
-    polychrome_sprites: Vec<PolychromeSprite>,
-    surfaces: Vec<Surface>,
+#[derive(Default, Copy, Clone, Debug, Eq, PartialEq, Hash)]
+#[repr(C)]
+pub struct ViewId {
+    low_bits: u32,
+    high_bits: u32,
 }
 
-impl SceneBuilder {
-    pub fn build(&mut self) -> Scene {
-        let mut orders = vec![0; self.layers_by_order.len()];
-        for (ix, layer_id) in self.layers_by_order.values().enumerate() {
-            orders[*layer_id as usize] = ix as u32;
-        }
-        self.layers_by_order.clear();
-        self.last_order = None;
-
-        for shadow in &mut self.shadows {
-            shadow.order = orders[shadow.order as usize];
-        }
-        self.shadows.sort_by_key(|shadow| shadow.order);
-
-        for quad in &mut self.quads {
-            quad.order = orders[quad.order as usize];
-        }
-        self.quads.sort_by_key(|quad| quad.order);
-
-        for path in &mut self.paths {
-            path.order = orders[path.order as usize];
+impl From<EntityId> for ViewId {
+    fn from(value: EntityId) -> Self {
+        let value = value.as_u64();
+        Self {
+            low_bits: value as u32,
+            high_bits: (value >> 32) as u32,
         }
-        self.paths.sort_by_key(|path| path.order);
+    }
+}
 
-        for underline in &mut self.underlines {
-            underline.order = orders[underline.order as usize];
-        }
-        self.underlines.sort_by_key(|underline| underline.order);
+impl From<ViewId> for EntityId {
+    fn from(value: ViewId) -> Self {
+        let value = (value.low_bits as u64) | ((value.high_bits as u64) << 32);
+        value.into()
+    }
+}
 
-        for monochrome_sprite in &mut self.monochrome_sprites {
-            monochrome_sprite.order = orders[monochrome_sprite.order as usize];
-        }
-        self.monochrome_sprites.sort_by_key(|sprite| sprite.order);
+#[derive(Default)]
+pub struct Scene {
+    layers_by_order: BTreeMap<StackingOrder, LayerId>,
+    orders_by_layer: BTreeMap<LayerId, StackingOrder>,
+    pub(crate) shadows: Vec<Shadow>,
+    pub(crate) quads: Vec<Quad>,
+    pub(crate) paths: Vec<Path<ScaledPixels>>,
+    pub(crate) underlines: Vec<Underline>,
+    pub(crate) monochrome_sprites: Vec<MonochromeSprite>,
+    pub(crate) polychrome_sprites: Vec<PolychromeSprite>,
+    pub(crate) surfaces: Vec<Surface>,
+}
 
-        for polychrome_sprite in &mut self.polychrome_sprites {
-            polychrome_sprite.order = orders[polychrome_sprite.order as usize];
-        }
-        self.polychrome_sprites.sort_by_key(|sprite| sprite.order);
+impl Scene {
+    pub fn clear(&mut self) {
+        self.layers_by_order.clear();
+        self.orders_by_layer.clear();
+        self.shadows.clear();
+        self.quads.clear();
+        self.paths.clear();
+        self.underlines.clear();
+        self.monochrome_sprites.clear();
+        self.polychrome_sprites.clear();
+        self.surfaces.clear();
+    }
 
-        for surface in &mut self.surfaces {
-            surface.order = orders[surface.order as usize];
-        }
-        self.surfaces.sort_by_key(|surface| surface.order);
+    pub fn paths(&self) -> &[Path<ScaledPixels>] {
+        &self.paths
+    }
 
-        Scene {
-            shadows: mem::take(&mut self.shadows),
-            quads: mem::take(&mut self.quads),
-            paths: mem::take(&mut self.paths),
-            underlines: mem::take(&mut self.underlines),
-            monochrome_sprites: mem::take(&mut self.monochrome_sprites),
-            polychrome_sprites: mem::take(&mut self.polychrome_sprites),
-            surfaces: mem::take(&mut self.surfaces),
+    pub(crate) fn batches(&self) -> impl Iterator<Item = PrimitiveBatch> {
+        BatchIterator {
+            shadows: &self.shadows,
+            shadows_start: 0,
+            shadows_iter: self.shadows.iter().peekable(),
+            quads: &self.quads,
+            quads_start: 0,
+            quads_iter: self.quads.iter().peekable(),
+            paths: &self.paths,
+            paths_start: 0,
+            paths_iter: self.paths.iter().peekable(),
+            underlines: &self.underlines,
+            underlines_start: 0,
+            underlines_iter: self.underlines.iter().peekable(),
+            monochrome_sprites: &self.monochrome_sprites,
+            monochrome_sprites_start: 0,
+            monochrome_sprites_iter: self.monochrome_sprites.iter().peekable(),
+            polychrome_sprites: &self.polychrome_sprites,
+            polychrome_sprites_start: 0,
+            polychrome_sprites_iter: self.polychrome_sprites.iter().peekable(),
+            surfaces: &self.surfaces,
+            surfaces_start: 0,
+            surfaces_iter: self.surfaces.iter().peekable(),
         }
     }
 
@@ -96,95 +107,139 @@ impl SceneBuilder {
         let layer_id = self.layer_id_for_order(order);
         match primitive {
             Primitive::Shadow(mut shadow) => {
-                shadow.order = layer_id;
+                shadow.layer_id = layer_id;
                 self.shadows.push(shadow);
             }
             Primitive::Quad(mut quad) => {
-                quad.order = layer_id;
+                quad.layer_id = layer_id;
                 self.quads.push(quad);
             }
             Primitive::Path(mut path) => {
-                path.order = layer_id;
+                path.layer_id = layer_id;
                 path.id = PathId(self.paths.len());
                 self.paths.push(path);
             }
             Primitive::Underline(mut underline) => {
-                underline.order = layer_id;
+                underline.layer_id = layer_id;
                 self.underlines.push(underline);
             }
             Primitive::MonochromeSprite(mut sprite) => {
-                sprite.order = layer_id;
+                sprite.layer_id = layer_id;
                 self.monochrome_sprites.push(sprite);
             }
             Primitive::PolychromeSprite(mut sprite) => {
-                sprite.order = layer_id;
+                sprite.layer_id = layer_id;
                 self.polychrome_sprites.push(sprite);
             }
             Primitive::Surface(mut surface) => {
-                surface.order = layer_id;
+                surface.layer_id = layer_id;
                 self.surfaces.push(surface);
             }
         }
     }
 
-    fn layer_id_for_order(&mut self, order: &StackingOrder) -> u32 {
-        if let Some((last_order, last_layer_id)) = self.last_order.as_ref() {
-            if last_order == order {
-                return *last_layer_id;
-            }
-        };
-
-        let layer_id = if let Some(layer_id) = self.layers_by_order.get(order) {
+    fn layer_id_for_order(&mut self, order: &StackingOrder) -> LayerId {
+        if let Some(layer_id) = self.layers_by_order.get(order) {
             *layer_id
         } else {
             let next_id = self.layers_by_order.len() as LayerId;
             self.layers_by_order.insert(order.clone(), next_id);
+            self.orders_by_layer.insert(next_id, order.clone());
             next_id
-        };
-        self.last_order = Some((order.clone(), layer_id));
-        layer_id
+        }
     }
-}
 
-pub struct Scene {
-    pub shadows: Vec<Shadow>,
-    pub quads: Vec<Quad>,
-    pub paths: Vec<Path<ScaledPixels>>,
-    pub underlines: Vec<Underline>,
-    pub monochrome_sprites: Vec<MonochromeSprite>,
-    pub polychrome_sprites: Vec<PolychromeSprite>,
-    pub surfaces: Vec<Surface>,
-}
+    pub fn reuse_views(&mut self, views: &FxHashSet<EntityId>, prev_scene: &mut Self) {
+        for shadow in prev_scene.shadows.drain(..) {
+            if views.contains(&shadow.view_id.into()) {
+                let order = &prev_scene.orders_by_layer[&shadow.layer_id];
+                self.insert(&order, shadow);
+            }
+        }
 
-impl Scene {
-    pub fn paths(&self) -> &[Path<ScaledPixels>] {
-        &self.paths
+        for quad in prev_scene.quads.drain(..) {
+            if views.contains(&quad.view_id.into()) {
+                let order = &prev_scene.orders_by_layer[&quad.layer_id];
+                self.insert(&order, quad);
+            }
+        }
+
+        for path in prev_scene.paths.drain(..) {
+            if views.contains(&path.view_id.into()) {
+                let order = &prev_scene.orders_by_layer[&path.layer_id];
+                self.insert(&order, path);
+            }
+        }
+
+        for underline in prev_scene.underlines.drain(..) {
+            if views.contains(&underline.view_id.into()) {
+                let order = &prev_scene.orders_by_layer[&underline.layer_id];
+                self.insert(&order, underline);
+            }
+        }
+
+        for sprite in prev_scene.monochrome_sprites.drain(..) {
+            if views.contains(&sprite.view_id.into()) {
+                let order = &prev_scene.orders_by_layer[&sprite.layer_id];
+                self.insert(&order, sprite);
+            }
+        }
+
+        for sprite in prev_scene.polychrome_sprites.drain(..) {
+            if views.contains(&sprite.view_id.into()) {
+                let order = &prev_scene.orders_by_layer[&sprite.layer_id];
+                self.insert(&order, sprite);
+            }
+        }
+
+        for surface in prev_scene.surfaces.drain(..) {
+            if views.contains(&surface.view_id.into()) {
+                let order = &prev_scene.orders_by_layer[&surface.layer_id];
+                self.insert(&order, surface);
+            }
+        }
     }
 
-    pub(crate) fn batches(&self) -> impl Iterator<Item = PrimitiveBatch> {
-        BatchIterator {
-            shadows: &self.shadows,
-            shadows_start: 0,
-            shadows_iter: self.shadows.iter().peekable(),
-            quads: &self.quads,
-            quads_start: 0,
-            quads_iter: self.quads.iter().peekable(),
-            paths: &self.paths,
-            paths_start: 0,
-            paths_iter: self.paths.iter().peekable(),
-            underlines: &self.underlines,
-            underlines_start: 0,
-            underlines_iter: self.underlines.iter().peekable(),
-            monochrome_sprites: &self.monochrome_sprites,
-            monochrome_sprites_start: 0,
-            monochrome_sprites_iter: self.monochrome_sprites.iter().peekable(),
-            polychrome_sprites: &self.polychrome_sprites,
-            polychrome_sprites_start: 0,
-            polychrome_sprites_iter: self.polychrome_sprites.iter().peekable(),
-            surfaces: &self.surfaces,
-            surfaces_start: 0,
-            surfaces_iter: self.surfaces.iter().peekable(),
+    pub fn finish(&mut self) {
+        let mut orders = vec![0; self.layers_by_order.len()];
+        for (ix, layer_id) in self.layers_by_order.values().enumerate() {
+            orders[*layer_id as usize] = ix as u32;
+        }
+
+        for shadow in &mut self.shadows {
+            shadow.order = orders[shadow.layer_id as usize];
+        }
+        self.shadows.sort_by_key(|shadow| shadow.order);
+
+        for quad in &mut self.quads {
+            quad.order = orders[quad.layer_id as usize];
+        }
+        self.quads.sort_by_key(|quad| quad.order);
+
+        for path in &mut self.paths {
+            path.order = orders[path.layer_id as usize];
+        }
+        self.paths.sort_by_key(|path| path.order);
+
+        for underline in &mut self.underlines {
+            underline.order = orders[underline.layer_id as usize];
+        }
+        self.underlines.sort_by_key(|underline| underline.order);
+
+        for monochrome_sprite in &mut self.monochrome_sprites {
+            monochrome_sprite.order = orders[monochrome_sprite.layer_id as usize];
         }
+        self.monochrome_sprites.sort_by_key(|sprite| sprite.order);
+
+        for polychrome_sprite in &mut self.polychrome_sprites {
+            polychrome_sprite.order = orders[polychrome_sprite.layer_id as usize];
+        }
+        self.polychrome_sprites.sort_by_key(|sprite| sprite.order);
+
+        for surface in &mut self.surfaces {
+            surface.order = orders[surface.layer_id as usize];
+        }
+        self.surfaces.sort_by_key(|surface| surface.order);
     }
 }
 
@@ -439,7 +494,9 @@ pub(crate) enum PrimitiveBatch<'a> {
 #[derive(Default, Debug, Clone, Eq, PartialEq)]
 #[repr(C)]
 pub struct Quad {
-    pub order: u32, // Initially a LayerId, then a DrawOrder.
+    pub view_id: ViewId,
+    pub layer_id: LayerId,
+    pub order: DrawOrder,
     pub bounds: Bounds<ScaledPixels>,
     pub content_mask: ContentMask<ScaledPixels>,
     pub background: Hsla,
@@ -469,7 +526,9 @@ impl From<Quad> for Primitive {
 #[derive(Debug, Clone, Eq, PartialEq)]
 #[repr(C)]
 pub struct Underline {
-    pub order: u32,
+    pub view_id: ViewId,
+    pub layer_id: LayerId,
+    pub order: DrawOrder,
     pub bounds: Bounds<ScaledPixels>,
     pub content_mask: ContentMask<ScaledPixels>,
     pub thickness: ScaledPixels,
@@ -498,7 +557,9 @@ impl From<Underline> for Primitive {
 #[derive(Debug, Clone, Eq, PartialEq)]
 #[repr(C)]
 pub struct Shadow {
-    pub order: u32,
+    pub view_id: ViewId,
+    pub layer_id: LayerId,
+    pub order: DrawOrder,
     pub bounds: Bounds<ScaledPixels>,
     pub corner_radii: Corners<ScaledPixels>,
     pub content_mask: ContentMask<ScaledPixels>,
@@ -527,7 +588,9 @@ impl From<Shadow> for Primitive {
 #[derive(Clone, Debug, Eq, PartialEq)]
 #[repr(C)]
 pub struct MonochromeSprite {
-    pub order: u32,
+    pub view_id: ViewId,
+    pub layer_id: LayerId,
+    pub order: DrawOrder,
     pub bounds: Bounds<ScaledPixels>,
     pub content_mask: ContentMask<ScaledPixels>,
     pub color: Hsla,
@@ -558,7 +621,9 @@ impl From<MonochromeSprite> for Primitive {
 #[derive(Clone, Debug, Eq, PartialEq)]
 #[repr(C)]
 pub struct PolychromeSprite {
-    pub order: u32,
+    pub view_id: ViewId,
+    pub layer_id: LayerId,
+    pub order: DrawOrder,
     pub bounds: Bounds<ScaledPixels>,
     pub content_mask: ContentMask<ScaledPixels>,
     pub corner_radii: Corners<ScaledPixels>,
@@ -589,7 +654,9 @@ impl From<PolychromeSprite> for Primitive {
 
 #[derive(Clone, Debug, Eq, PartialEq)]
 pub struct Surface {
-    pub order: u32,
+    pub view_id: ViewId,
+    pub layer_id: LayerId,
+    pub order: DrawOrder,
     pub bounds: Bounds<ScaledPixels>,
     pub content_mask: ContentMask<ScaledPixels>,
     pub image_buffer: media::core_video::CVImageBuffer,
@@ -619,7 +686,9 @@ pub(crate) struct PathId(pub(crate) usize);
 #[derive(Debug)]
 pub struct Path<P: Clone + Default + Debug> {
     pub(crate) id: PathId,
-    order: u32,
+    pub(crate) view_id: ViewId,
+    layer_id: LayerId,
+    order: DrawOrder,
     pub(crate) bounds: Bounds<P>,
     pub(crate) content_mask: ContentMask<P>,
     pub(crate) vertices: Vec<PathVertex<P>>,
@@ -633,7 +702,9 @@ impl Path<Pixels> {
     pub fn new(start: Point<Pixels>) -> Self {
         Self {
             id: PathId(0),
-            order: 0,
+            view_id: ViewId::default(),
+            layer_id: LayerId::default(),
+            order: DrawOrder::default(),
             vertices: Vec::new(),
             start,
             current: start,
@@ -650,6 +721,8 @@ impl Path<Pixels> {
     pub fn scale(&self, factor: f32) -> Path<ScaledPixels> {
         Path {
             id: self.id,
+            view_id: self.view_id,
+            layer_id: self.layer_id,
             order: self.order,
             bounds: self.bounds.scale(factor),
             content_mask: self.content_mask.scale(factor),

crates/gpui/src/style.rs 🔗

@@ -1,10 +1,10 @@
 use std::{iter, mem, ops::Range};
 
 use crate::{
-    black, phi, point, quad, rems, AbsoluteLength, BorrowWindow, Bounds, ContentMask, Corners,
-    CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement, Font, FontFeatures,
-    FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba, SharedString, Size,
-    SizeRefinement, Styled, TextRun, WindowContext,
+    black, phi, point, quad, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds,
+    ContentMask, Corners, CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement,
+    Font, FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba,
+    SharedString, Size, SizeRefinement, Styled, TextRun, WindowContext,
 };
 use collections::HashSet;
 use refineable::{Cascade, Refineable};
@@ -146,7 +146,7 @@ pub enum WhiteSpace {
     Nowrap,
 }
 
-#[derive(Refineable, Clone, Debug)]
+#[derive(Refineable, Clone, Debug, PartialEq)]
 #[refineable(Debug)]
 pub struct TextStyle {
     pub color: Hsla,
@@ -308,54 +308,54 @@ impl Style {
         }
     }
 
-    // pub fn apply_text_style<C, F, R>(&self, cx: &mut C, f: F) -> R
-    // where
-    //     C: BorrowAppContext,
-    //     F: FnOnce(&mut C) -> R,
-    // {
-    //     if self.text.is_some() {
-    //         cx.with_text_style(Some(self.text.clone()), f)
-    //     } else {
-    //         f(cx)
-    //     }
-    // }
-
-    // /// Apply overflow to content mask
-    // pub fn apply_overflow<C, F, R>(&self, bounds: Bounds<Pixels>, cx: &mut C, f: F) -> R
-    // where
-    //     C: BorrowWindow,
-    //     F: FnOnce(&mut C) -> R,
-    // {
-    //     let current_mask = cx.content_mask();
-
-    //     let min = current_mask.bounds.origin;
-    //     let max = current_mask.bounds.lower_right();
-
-    //     let mask_bounds = match (
-    //         self.overflow.x == Overflow::Visible,
-    //         self.overflow.y == Overflow::Visible,
-    //     ) {
-    //         // x and y both visible
-    //         (true, true) => return f(cx),
-    //         // x visible, y hidden
-    //         (true, false) => Bounds::from_corners(
-    //             point(min.x, bounds.origin.y),
-    //             point(max.x, bounds.lower_right().y),
-    //         ),
-    //         // x hidden, y visible
-    //         (false, true) => Bounds::from_corners(
-    //             point(bounds.origin.x, min.y),
-    //             point(bounds.lower_right().x, max.y),
-    //         ),
-    //         // both hidden
-    //         (false, false) => bounds,
-    //     };
-    //     let mask = ContentMask {
-    //         bounds: mask_bounds,
-    //     };
-
-    //     cx.with_content_mask(Some(mask), f)
-    // }
+    pub fn apply_text_style<C, F, R>(&self, cx: &mut C, f: F) -> R
+    where
+        C: BorrowAppContext,
+        F: FnOnce(&mut C) -> R,
+    {
+        if self.text.is_some() {
+            cx.with_text_style(Some(self.text.clone()), f)
+        } else {
+            f(cx)
+        }
+    }
+
+    /// Apply overflow to content mask
+    pub fn apply_overflow<C, F, R>(&self, bounds: Bounds<Pixels>, cx: &mut C, f: F) -> R
+    where
+        C: BorrowWindow,
+        F: FnOnce(&mut C) -> R,
+    {
+        let current_mask = cx.content_mask();
+
+        let min = current_mask.bounds.origin;
+        let max = current_mask.bounds.lower_right();
+
+        let mask_bounds = match (
+            self.overflow.x == Overflow::Visible,
+            self.overflow.y == Overflow::Visible,
+        ) {
+            // x and y both visible
+            (true, true) => return f(cx),
+            // x visible, y hidden
+            (true, false) => Bounds::from_corners(
+                point(min.x, bounds.origin.y),
+                point(max.x, bounds.lower_right().y),
+            ),
+            // x hidden, y visible
+            (false, true) => Bounds::from_corners(
+                point(bounds.origin.x, min.y),
+                point(bounds.lower_right().x, max.y),
+            ),
+            // both hidden
+            (false, false) => bounds,
+        };
+        let mask = ContentMask {
+            bounds: mask_bounds,
+        };
+
+        cx.with_content_mask(Some(mask), f)
+    }
 
     /// Paints the background of an element styled with this style.
     pub fn paint(

crates/gpui/src/styled.rs 🔗

@@ -1,7 +1,7 @@
 use crate::{
     self as gpui, hsla, point, px, relative, rems, AbsoluteLength, AlignItems, CursorStyle,
-    DefiniteLength, Display, Fill, FlexDirection, Hsla, JustifyContent, Length, Position,
-    SharedString, StyleRefinement, Visibility, WhiteSpace,
+    DefiniteLength, Display, Fill, FlexDirection, FontWeight, Hsla, JustifyContent, Length,
+    Position, SharedString, StyleRefinement, Visibility, WhiteSpace,
 };
 use crate::{BoxShadow, TextStyleRefinement};
 use smallvec::{smallvec, SmallVec};
@@ -494,6 +494,13 @@ pub trait Styled: Sized {
         self
     }
 
+    fn font_weight(mut self, weight: FontWeight) -> Self {
+        self.text_style()
+            .get_or_insert_with(Default::default)
+            .font_weight = Some(weight);
+        self
+    }
+
     fn text_bg(mut self, bg: impl Into<Hsla>) -> Self {
         self.text_style()
             .get_or_insert_with(Default::default)

crates/gpui/src/taffy.rs 🔗

@@ -14,6 +14,7 @@ use taffy::{
 
 pub struct TaffyLayoutEngine {
     taffy: Taffy,
+    styles: FxHashMap<LayoutId, Style>,
     children_to_parents: FxHashMap<LayoutId, LayoutId>,
     absolute_layout_bounds: FxHashMap<LayoutId, Bounds<Pixels>>,
     computed_layouts: FxHashSet<LayoutId>,
@@ -35,6 +36,7 @@ impl TaffyLayoutEngine {
     pub fn new() -> Self {
         TaffyLayoutEngine {
             taffy: Taffy::new(),
+            styles: FxHashMap::default(),
             children_to_parents: FxHashMap::default(),
             absolute_layout_bounds: FxHashMap::default(),
             computed_layouts: FxHashSet::default(),
@@ -48,6 +50,11 @@ impl TaffyLayoutEngine {
         self.absolute_layout_bounds.clear();
         self.computed_layouts.clear();
         self.nodes_to_measure.clear();
+        self.styles.clear();
+    }
+
+    pub fn requested_style(&self, layout_id: LayoutId) -> Option<&Style> {
+        self.styles.get(&layout_id)
     }
 
     pub fn request_layout(
@@ -56,21 +63,26 @@ impl TaffyLayoutEngine {
         rem_size: Pixels,
         children: &[LayoutId],
     ) -> LayoutId {
-        let style = style.to_taffy(rem_size);
-        if children.is_empty() {
-            self.taffy.new_leaf(style).expect(EXPECT_MESSAGE).into()
+        let taffy_style = style.to_taffy(rem_size);
+        let layout_id = if children.is_empty() {
+            self.taffy
+                .new_leaf(taffy_style)
+                .expect(EXPECT_MESSAGE)
+                .into()
         } else {
             let parent_id = self
                 .taffy
                 // This is safe because LayoutId is repr(transparent) to taffy::tree::NodeId.
-                .new_with_children(style, unsafe { std::mem::transmute(children) })
+                .new_with_children(taffy_style, unsafe { std::mem::transmute(children) })
                 .expect(EXPECT_MESSAGE)
                 .into();
             for child_id in children {
                 self.children_to_parents.insert(*child_id, parent_id);
             }
             parent_id
-        }
+        };
+        self.styles.insert(layout_id, style.clone());
+        layout_id
     }
 
     pub fn request_measured_layout(
@@ -80,14 +92,16 @@ impl TaffyLayoutEngine {
         measure: impl FnMut(Size<Option<Pixels>>, Size<AvailableSpace>, &mut WindowContext) -> Size<Pixels>
             + 'static,
     ) -> LayoutId {
-        let style = style.to_taffy(rem_size);
+        let style = style.clone();
+        let taffy_style = style.to_taffy(rem_size);
 
         let layout_id = self
             .taffy
-            .new_leaf_with_context(style, ())
+            .new_leaf_with_context(taffy_style, ())
             .expect(EXPECT_MESSAGE)
             .into();
         self.nodes_to_measure.insert(layout_id, Box::new(measure));
+        self.styles.insert(layout_id, style.clone());
         layout_id
     }
 
@@ -271,20 +285,6 @@ impl ToTaffy<taffy::style::Style> for Style {
     }
 }
 
-// impl ToTaffy for Bounds<Length> {
-//     type Output = taffy::prelude::Bounds<taffy::prelude::LengthPercentageAuto>;
-
-//     fn to_taffy(
-//         &self,
-//         rem_size: Pixels,
-//     ) -> taffy::prelude::Bounds<taffy::prelude::LengthPercentageAuto> {
-//         taffy::prelude::Bounds {
-//             origin: self.origin.to_taffy(rem_size),
-//             size: self.size.to_taffy(rem_size),
-//         }
-//     }
-// }
-
 impl ToTaffy<taffy::style::LengthPercentageAuto> for Length {
     fn to_taffy(&self, rem_size: Pixels) -> taffy::prelude::LengthPercentageAuto {
         match self {

crates/gpui/src/text_system.rs 🔗

@@ -9,11 +9,11 @@ pub use line_layout::*;
 pub use line_wrapper::*;
 
 use crate::{
-    px, Bounds, DevicePixels, Hsla, Pixels, PlatformTextSystem, Point, Result, SharedString, Size,
-    UnderlineStyle,
+    px, Bounds, DevicePixels, EntityId, Hsla, Pixels, PlatformTextSystem, Point, Result,
+    SharedString, Size, UnderlineStyle,
 };
 use anyhow::anyhow;
-use collections::FxHashMap;
+use collections::{FxHashMap, FxHashSet};
 use core::fmt;
 use itertools::Itertools;
 use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
@@ -65,6 +65,17 @@ impl TextSystem {
         }
     }
 
+    pub fn all_font_families(&self) -> Vec<String> {
+        let mut families = self.platform_text_system.all_font_families();
+        families.append(
+            &mut self
+                .fallback_font_stack
+                .iter()
+                .map(|font| font.family.to_string())
+                .collect(),
+        );
+        families
+    }
     pub fn add_fonts(&self, fonts: &[Arc<Vec<u8>>]) -> Result<()> {
         self.platform_text_system.add_fonts(fonts)
     }
@@ -186,6 +197,10 @@ impl TextSystem {
         }
     }
 
+    pub fn with_view<R>(&self, view_id: EntityId, f: impl FnOnce() -> R) -> R {
+        self.line_layout_cache.with_view(view_id, f)
+    }
+
     pub fn layout_line(
         &self,
         text: &str,
@@ -360,8 +375,8 @@ impl TextSystem {
         Ok(lines)
     }
 
-    pub fn start_frame(&self) {
-        self.line_layout_cache.start_frame()
+    pub fn finish_frame(&self, reused_views: &FxHashSet<EntityId>) {
+        self.line_layout_cache.finish_frame(reused_views)
     }
 
     pub fn line_wrapper(self: &Arc<Self>, font: Font, font_size: Pixels) -> LineWrapperHandle {

crates/gpui/src/text_system/line_layout.rs 🔗

@@ -1,5 +1,5 @@
-use crate::{px, FontId, GlyphId, Pixels, PlatformTextSystem, Point, Size};
-use collections::FxHashMap;
+use crate::{px, EntityId, FontId, GlyphId, Pixels, PlatformTextSystem, Point, Size};
+use collections::{FxHashMap, FxHashSet};
 use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
 use smallvec::SmallVec;
 use std::{
@@ -236,6 +236,7 @@ impl WrappedLineLayout {
 }
 
 pub(crate) struct LineLayoutCache {
+    view_stack: Mutex<Vec<EntityId>>,
     previous_frame: Mutex<FxHashMap<CacheKey, Arc<LineLayout>>>,
     current_frame: RwLock<FxHashMap<CacheKey, Arc<LineLayout>>>,
     previous_frame_wrapped: Mutex<FxHashMap<CacheKey, Arc<WrappedLineLayout>>>,
@@ -246,6 +247,7 @@ pub(crate) struct LineLayoutCache {
 impl LineLayoutCache {
     pub fn new(platform_text_system: Arc<dyn PlatformTextSystem>) -> Self {
         Self {
+            view_stack: Mutex::default(),
             previous_frame: Mutex::default(),
             current_frame: RwLock::default(),
             previous_frame_wrapped: Mutex::default(),
@@ -254,11 +256,43 @@ impl LineLayoutCache {
         }
     }
 
-    pub fn start_frame(&self) {
+    pub fn finish_frame(&self, reused_views: &FxHashSet<EntityId>) {
+        debug_assert_eq!(self.view_stack.lock().len(), 0);
+
         let mut prev_frame = self.previous_frame.lock();
         let mut curr_frame = self.current_frame.write();
+        for (key, layout) in prev_frame.drain() {
+            if key
+                .parent_view_id
+                .map_or(false, |view_id| reused_views.contains(&view_id))
+            {
+                curr_frame.insert(key, layout);
+            }
+        }
         std::mem::swap(&mut *prev_frame, &mut *curr_frame);
-        curr_frame.clear();
+
+        let mut prev_frame_wrapped = self.previous_frame_wrapped.lock();
+        let mut curr_frame_wrapped = self.current_frame_wrapped.write();
+        for (key, layout) in prev_frame_wrapped.drain() {
+            if key
+                .parent_view_id
+                .map_or(false, |view_id| reused_views.contains(&view_id))
+            {
+                curr_frame_wrapped.insert(key, layout);
+            }
+        }
+        std::mem::swap(&mut *prev_frame_wrapped, &mut *curr_frame_wrapped);
+    }
+
+    pub fn with_view<R>(&self, view_id: EntityId, f: impl FnOnce() -> R) -> R {
+        self.view_stack.lock().push(view_id);
+        let result = f();
+        self.view_stack.lock().pop();
+        result
+    }
+
+    fn parent_view_id(&self) -> Option<EntityId> {
+        self.view_stack.lock().last().copied()
     }
 
     pub fn layout_wrapped_line(
@@ -273,6 +307,7 @@ impl LineLayoutCache {
             font_size,
             runs,
             wrap_width,
+            parent_view_id: self.parent_view_id(),
         } as &dyn AsCacheKeyRef;
 
         let current_frame = self.current_frame_wrapped.upgradable_read();
@@ -301,6 +336,7 @@ impl LineLayoutCache {
                 font_size,
                 runs: SmallVec::from(runs),
                 wrap_width,
+                parent_view_id: self.parent_view_id(),
             };
             current_frame.insert(key, layout.clone());
             layout
@@ -313,6 +349,7 @@ impl LineLayoutCache {
             font_size,
             runs,
             wrap_width: None,
+            parent_view_id: self.parent_view_id(),
         } as &dyn AsCacheKeyRef;
 
         let current_frame = self.current_frame.upgradable_read();
@@ -331,6 +368,7 @@ impl LineLayoutCache {
                 font_size,
                 runs: SmallVec::from(runs),
                 wrap_width: None,
+                parent_view_id: self.parent_view_id(),
             };
             current_frame.insert(key, layout.clone());
             layout
@@ -348,12 +386,13 @@ trait AsCacheKeyRef {
     fn as_cache_key_ref(&self) -> CacheKeyRef;
 }
 
-#[derive(Eq)]
+#[derive(Debug, Eq)]
 struct CacheKey {
     text: String,
     font_size: Pixels,
     runs: SmallVec<[FontRun; 1]>,
     wrap_width: Option<Pixels>,
+    parent_view_id: Option<EntityId>,
 }
 
 #[derive(Copy, Clone, PartialEq, Eq, Hash)]
@@ -362,6 +401,7 @@ struct CacheKeyRef<'a> {
     font_size: Pixels,
     runs: &'a [FontRun],
     wrap_width: Option<Pixels>,
+    parent_view_id: Option<EntityId>,
 }
 
 impl<'a> PartialEq for (dyn AsCacheKeyRef + 'a) {
@@ -385,6 +425,7 @@ impl AsCacheKeyRef for CacheKey {
             font_size: self.font_size,
             runs: self.runs.as_slice(),
             wrap_width: self.wrap_width,
+            parent_view_id: self.parent_view_id,
         }
     }
 }

crates/gpui/src/view.rs 🔗

@@ -1,8 +1,8 @@
 use crate::{
     seal::Sealed, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace, BorrowWindow,
-    Bounds, Element, ElementId, Entity, EntityId, Flatten, FocusHandle, FocusableView, IntoElement,
-    LayoutId, Model, Pixels, Point, Render, Size, ViewContext, VisualContext, WeakModel,
-    WindowContext,
+    Bounds, ContentMask, Element, ElementId, Entity, EntityId, Flatten, FocusHandle, FocusableView,
+    IntoElement, LayoutId, Model, Pixels, Point, Render, Size, StackingOrder, Style, TextStyle,
+    ViewContext, VisualContext, WeakModel, WindowContext,
 };
 use anyhow::{Context, Result};
 use std::{
@@ -17,6 +17,19 @@ pub struct View<V> {
 
 impl<V> Sealed for View<V> {}
 
+pub struct AnyViewState {
+    root_style: Style,
+    cache_key: Option<ViewCacheKey>,
+    element: Option<AnyElement>,
+}
+
+struct ViewCacheKey {
+    bounds: Bounds<Pixels>,
+    stacking_order: StackingOrder,
+    content_mask: ContentMask<Pixels>,
+    text_style: TextStyle,
+}
+
 impl<V: 'static> Entity<V> for View<V> {
     type Weak = WeakView<V>;
 
@@ -60,16 +73,6 @@ impl<V: 'static> View<V> {
         self.model.read(cx)
     }
 
-    // pub fn render_with<E>(&self, component: E) -> RenderViewWith<E, V>
-    // where
-    //     E: 'static + Element,
-    // {
-    //     RenderViewWith {
-    //         view: self.clone(),
-    //         element: Some(component),
-    //     }
-    // }
-
     pub fn focus_handle(&self, cx: &AppContext) -> FocusHandle
     where
         V: FocusableView,
@@ -86,13 +89,15 @@ impl<V: Render> Element for View<V> {
         _state: Option<Self::State>,
         cx: &mut WindowContext,
     ) -> (LayoutId, Self::State) {
-        let mut element = self.update(cx, |view, cx| view.render(cx).into_any_element());
-        let layout_id = element.request_layout(cx);
-        (layout_id, Some(element))
+        cx.with_view_id(self.entity_id(), |cx| {
+            let mut element = self.update(cx, |view, cx| view.render(cx).into_any_element());
+            let layout_id = element.request_layout(cx);
+            (layout_id, Some(element))
+        })
     }
 
     fn paint(&mut self, _: Bounds<Pixels>, element: &mut Self::State, cx: &mut WindowContext) {
-        element.take().unwrap().paint(cx);
+        cx.paint_view(self.entity_id(), |cx| element.take().unwrap().paint(cx));
     }
 }
 
@@ -183,16 +188,20 @@ impl<V> Eq for WeakView<V> {}
 #[derive(Clone, Debug)]
 pub struct AnyView {
     model: AnyModel,
-    layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, AnyElement),
-    paint: fn(&AnyView, &mut AnyElement, &mut WindowContext),
+    request_layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, AnyElement),
+    cache: bool,
 }
 
 impl AnyView {
+    pub fn cached(mut self) -> Self {
+        self.cache = true;
+        self
+    }
+
     pub fn downgrade(&self) -> AnyWeakView {
         AnyWeakView {
             model: self.model.downgrade(),
-            layout: self.layout,
-            paint: self.paint,
+            layout: self.request_layout,
         }
     }
 
@@ -201,8 +210,8 @@ impl AnyView {
             Ok(model) => Ok(View { model }),
             Err(model) => Err(Self {
                 model,
-                layout: self.layout,
-                paint: self.paint,
+                request_layout: self.request_layout,
+                cache: self.cache,
             }),
         }
     }
@@ -221,10 +230,12 @@ impl AnyView {
         available_space: Size<AvailableSpace>,
         cx: &mut WindowContext,
     ) {
-        cx.with_absolute_element_offset(origin, |cx| {
-            let (layout_id, mut rendered_element) = (self.layout)(self, cx);
-            cx.compute_layout(layout_id, available_space);
-            (self.paint)(self, &mut rendered_element, cx);
+        cx.paint_view(self.entity_id(), |cx| {
+            cx.with_absolute_element_offset(origin, |cx| {
+                let (layout_id, mut rendered_element) = (self.request_layout)(self, cx);
+                cx.compute_layout(layout_id, available_space);
+                rendered_element.paint(cx)
+            });
         })
     }
 }
@@ -233,30 +244,72 @@ impl<V: Render> From<View<V>> for AnyView {
     fn from(value: View<V>) -> Self {
         AnyView {
             model: value.model.into_any(),
-            layout: any_view::layout::<V>,
-            paint: any_view::paint,
+            request_layout: any_view::request_layout::<V>,
+            cache: false,
         }
     }
 }
 
 impl Element for AnyView {
-    type State = Option<AnyElement>;
+    type State = AnyViewState;
 
     fn request_layout(
         &mut self,
-        _state: Option<Self::State>,
+        state: Option<Self::State>,
         cx: &mut WindowContext,
     ) -> (LayoutId, Self::State) {
-        let (layout_id, state) = (self.layout)(self, cx);
-        (layout_id, Some(state))
+        cx.with_view_id(self.entity_id(), |cx| {
+            if self.cache {
+                if let Some(state) = state {
+                    let layout_id = cx.request_layout(&state.root_style, None);
+                    return (layout_id, state);
+                }
+            }
+
+            let (layout_id, element) = (self.request_layout)(self, cx);
+            let root_style = cx.layout_style(layout_id).unwrap().clone();
+            let state = AnyViewState {
+                root_style,
+                cache_key: None,
+                element: Some(element),
+            };
+            (layout_id, state)
+        })
     }
 
-    fn paint(&mut self, _: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
-        debug_assert!(
-            state.is_some(),
-            "state is None. Did you include an AnyView twice in the tree?"
-        );
-        (self.paint)(self, state.as_mut().unwrap(), cx)
+    fn paint(&mut self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
+        cx.paint_view(self.entity_id(), |cx| {
+            if !self.cache {
+                state.element.take().unwrap().paint(cx);
+                return;
+            }
+
+            if let Some(cache_key) = state.cache_key.as_mut() {
+                if cache_key.bounds == bounds
+                    && cache_key.content_mask == cx.content_mask()
+                    && cache_key.stacking_order == *cx.stacking_order()
+                    && cache_key.text_style == cx.text_style()
+                    && !cx.window.dirty_views.contains(&self.entity_id())
+                    && !cx.window.refreshing
+                {
+                    cx.reuse_view();
+                    return;
+                }
+            }
+
+            let mut element = state
+                .element
+                .take()
+                .unwrap_or_else(|| (self.request_layout)(self, cx).1);
+            element.draw(bounds.origin, bounds.size.into(), cx);
+
+            state.cache_key = Some(ViewCacheKey {
+                bounds,
+                stacking_order: cx.stacking_order().clone(),
+                content_mask: cx.content_mask(),
+                text_style: cx.text_style(),
+            });
+        })
     }
 }
 
@@ -287,7 +340,6 @@ impl IntoElement for AnyView {
 pub struct AnyWeakView {
     model: AnyWeakModel,
     layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, AnyElement),
-    paint: fn(&AnyView, &mut AnyElement, &mut WindowContext),
 }
 
 impl AnyWeakView {
@@ -295,8 +347,8 @@ impl AnyWeakView {
         let model = self.model.upgrade()?;
         Some(AnyView {
             model,
-            layout: self.layout,
-            paint: self.paint,
+            request_layout: self.layout,
+            cache: false,
         })
     }
 }
@@ -305,8 +357,7 @@ impl<V: 'static + Render> From<WeakView<V>> for AnyWeakView {
     fn from(view: WeakView<V>) -> Self {
         Self {
             model: view.model.into(),
-            layout: any_view::layout::<V>,
-            paint: any_view::paint,
+            layout: any_view::request_layout::<V>,
         }
     }
 }
@@ -328,7 +379,7 @@ impl std::fmt::Debug for AnyWeakView {
 mod any_view {
     use crate::{AnyElement, AnyView, IntoElement, LayoutId, Render, WindowContext};
 
-    pub(crate) fn layout<V: 'static + Render>(
+    pub(crate) fn request_layout<V: 'static + Render>(
         view: &AnyView,
         cx: &mut WindowContext,
     ) -> (LayoutId, AnyElement) {
@@ -337,8 +388,4 @@ mod any_view {
         let layout_id = element.request_layout(cx);
         (layout_id, element)
     }
-
-    pub(crate) fn paint(_view: &AnyView, element: &mut AnyElement, cx: &mut WindowContext) {
-        element.paint(cx);
-    }
 }

crates/gpui/src/window.rs 🔗

@@ -1,7 +1,7 @@
 #![deny(missing_docs)]
 
 use crate::{
-    px, size, transparent_black, Action, AnyDrag, AnyView, AppContext, Arena, ArenaBox, ArenaRef,
+    px, size, transparent_black, Action, AnyDrag, AnyTooltip, AnyView, AppContext, Arena,
     AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle,
     DevicePixels, DispatchActionListener, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect,
     Entity, EntityId, EventEmitter, FileDropEvent, Flatten, FontId, GlobalElementId, GlyphId, Hsla,
@@ -9,12 +9,12 @@ use crate::{
     Model, ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseMoveEvent, MouseUpEvent,
     Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point,
     PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams,
-    RenderSvgParams, ScaledPixels, Scene, SceneBuilder, Shadow, SharedString, Size, Style,
-    SubscriberSet, Subscription, Surface, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View,
-    VisualContext, WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS,
+    RenderSvgParams, ScaledPixels, Scene, Shadow, SharedString, Size, Style, SubscriberSet,
+    Subscription, Surface, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext,
+    WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS,
 };
 use anyhow::{anyhow, Context as _, Result};
-use collections::FxHashMap;
+use collections::{FxHashMap, FxHashSet};
 use derive_more::{Deref, DerefMut};
 use futures::{
     channel::{mpsc, oneshot},
@@ -99,7 +99,7 @@ impl DispatchPhase {
 }
 
 type AnyObserver = Box<dyn FnMut(&mut WindowContext) -> bool + 'static>;
-type AnyMouseListener = ArenaBox<dyn FnMut(&dyn Any, DispatchPhase, &mut WindowContext) + 'static>;
+type AnyMouseListener = Box<dyn FnMut(&dyn Any, DispatchPhase, &mut WindowContext) + 'static>;
 type AnyWindowFocusListener = Box<dyn FnMut(&FocusEvent, &mut WindowContext) -> bool + 'static>;
 
 struct FocusEvent {
@@ -266,19 +266,19 @@ pub struct Window {
     pub(crate) element_id_stack: GlobalElementId,
     pub(crate) rendered_frame: Frame,
     pub(crate) next_frame: Frame,
-    frame_arena: Arena,
+    pub(crate) dirty_views: FxHashSet<EntityId>,
     pub(crate) focus_handles: Arc<RwLock<SlotMap<FocusId, AtomicUsize>>>,
     focus_listeners: SubscriberSet<(), AnyWindowFocusListener>,
     focus_lost_listeners: SubscriberSet<(), AnyObserver>,
     default_prevented: bool,
     mouse_position: Point<Pixels>,
     modifiers: Modifiers,
-    requested_cursor_style: Option<CursorStyle>,
     scale_factor: f32,
     bounds: WindowBounds,
     bounds_observers: SubscriberSet<(), AnyObserver>,
     active: bool,
     pub(crate) dirty: bool,
+    pub(crate) refreshing: bool,
     pub(crate) drawing: bool,
     activation_observers: SubscriberSet<(), AnyObserver>,
     pub(crate) focus: Option<FocusId>,
@@ -290,22 +290,39 @@ pub struct Window {
 
 pub(crate) struct ElementStateBox {
     inner: Box<dyn Any>,
+    parent_view_id: EntityId,
     #[cfg(debug_assertions)]
     type_name: &'static str,
 }
 
+struct RequestedInputHandler {
+    view_id: EntityId,
+    handler: Option<Box<dyn PlatformInputHandler>>,
+}
+
+struct TooltipRequest {
+    view_id: EntityId,
+    tooltip: AnyTooltip,
+}
+
 pub(crate) struct Frame {
     focus: Option<FocusId>,
     window_active: bool,
     pub(crate) element_states: FxHashMap<GlobalElementId, ElementStateBox>,
-    mouse_listeners: FxHashMap<TypeId, Vec<(StackingOrder, AnyMouseListener)>>,
+    mouse_listeners: FxHashMap<TypeId, Vec<(StackingOrder, EntityId, AnyMouseListener)>>,
     pub(crate) dispatch_tree: DispatchTree,
-    pub(crate) scene_builder: SceneBuilder,
-    pub(crate) depth_map: Vec<(StackingOrder, Bounds<Pixels>)>,
+    pub(crate) scene: Scene,
+    pub(crate) depth_map: Vec<(StackingOrder, EntityId, Bounds<Pixels>)>,
     pub(crate) z_index_stack: StackingOrder,
     pub(crate) next_stacking_order_id: u32,
     content_mask_stack: Vec<ContentMask<Pixels>>,
     element_offset_stack: Vec<Point<Pixels>>,
+    requested_input_handler: Option<RequestedInputHandler>,
+    tooltip_request: Option<TooltipRequest>,
+    cursor_styles: FxHashMap<EntityId, CursorStyle>,
+    requested_cursor_style: Option<CursorStyle>,
+    pub(crate) view_stack: Vec<EntityId>,
+    pub(crate) reused_views: FxHashSet<EntityId>,
 }
 
 impl Frame {
@@ -316,12 +333,18 @@ impl Frame {
             element_states: FxHashMap::default(),
             mouse_listeners: FxHashMap::default(),
             dispatch_tree,
-            scene_builder: SceneBuilder::default(),
+            scene: Scene::default(),
+            depth_map: Vec::new(),
             z_index_stack: StackingOrder::default(),
             next_stacking_order_id: 0,
-            depth_map: Default::default(),
             content_mask_stack: Vec::new(),
             element_offset_stack: Vec::new(),
+            requested_input_handler: None,
+            tooltip_request: None,
+            cursor_styles: FxHashMap::default(),
+            requested_cursor_style: None,
+            view_stack: Vec::new(),
+            reused_views: FxHashSet::default(),
         }
     }
 
@@ -331,6 +354,13 @@ impl Frame {
         self.dispatch_tree.clear();
         self.depth_map.clear();
         self.next_stacking_order_id = 0;
+        self.reused_views.clear();
+        self.scene.clear();
+        self.requested_input_handler.take();
+        self.tooltip_request.take();
+        self.cursor_styles.clear();
+        self.requested_cursor_style.take();
+        debug_assert_eq!(self.view_stack.len(), 0);
     }
 
     fn focus_path(&self) -> SmallVec<[FocusId; 8]> {
@@ -338,6 +368,42 @@ impl Frame {
             .map(|focus_id| self.dispatch_tree.focus_path(focus_id))
             .unwrap_or_default()
     }
+
+    fn finish(&mut self, prev_frame: &mut Self) {
+        // Reuse mouse listeners that didn't change since the last frame.
+        for (type_id, listeners) in &mut prev_frame.mouse_listeners {
+            let next_listeners = self.mouse_listeners.entry(*type_id).or_default();
+            for (order, view_id, listener) in listeners.drain(..) {
+                if self.reused_views.contains(&view_id) {
+                    next_listeners.push((order, view_id, listener));
+                }
+            }
+        }
+
+        // Reuse entries in the depth map that didn't change since the last frame.
+        for (order, view_id, bounds) in prev_frame.depth_map.drain(..) {
+            if self.reused_views.contains(&view_id) {
+                match self
+                    .depth_map
+                    .binary_search_by(|(level, _, _)| order.cmp(level))
+                {
+                    Ok(i) | Err(i) => self.depth_map.insert(i, (order, view_id, bounds)),
+                }
+            }
+        }
+
+        // Retain element states for views that didn't change since the last frame.
+        for (element_id, state) in prev_frame.element_states.drain() {
+            if self.reused_views.contains(&state.parent_view_id) {
+                self.element_states.entry(element_id).or_insert(state);
+            }
+        }
+
+        // Reuse geometry that didn't change since the last frame.
+        self.scene
+            .reuse_views(&self.reused_views, &mut prev_frame.scene);
+        self.scene.finish();
+    }
 }
 
 impl Window {
@@ -346,14 +412,7 @@ impl Window {
         options: WindowOptions,
         cx: &mut AppContext,
     ) -> Self {
-        let platform_window = cx.platform.open_window(
-            handle,
-            options,
-            Box::new({
-                let mut cx = cx.to_async();
-                move || handle.update(&mut cx, |_, cx| cx.draw())
-            }),
-        );
+        let platform_window = cx.platform.open_window(handle, options);
         let display_id = platform_window.display().id();
         let sprite_atlas = platform_window.sprite_atlas();
         let mouse_position = platform_window.mouse_position();
@@ -362,6 +421,12 @@ impl Window {
         let scale_factor = platform_window.scale_factor();
         let bounds = platform_window.bounds();
 
+        platform_window.on_request_frame(Box::new({
+            let mut cx = cx.to_async();
+            move || {
+                handle.update(&mut cx, |_, cx| cx.draw()).log_err();
+            }
+        }));
         platform_window.on_resize(Box::new({
             let mut cx = cx.to_async();
             move |_, _| {
@@ -416,19 +481,19 @@ impl Window {
             element_id_stack: GlobalElementId::default(),
             rendered_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())),
             next_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())),
-            frame_arena: Arena::new(1024 * 1024),
+            dirty_views: FxHashSet::default(),
             focus_handles: Arc::new(RwLock::new(SlotMap::with_key())),
             focus_listeners: SubscriberSet::new(),
             focus_lost_listeners: SubscriberSet::new(),
             default_prevented: true,
             mouse_position,
             modifiers,
-            requested_cursor_style: None,
             scale_factor,
             bounds,
             bounds_observers: SubscriberSet::new(),
             active: false,
             dirty: false,
+            refreshing: false,
             drawing: false,
             activation_observers: SubscriberSet::new(),
             focus: None,
@@ -484,8 +549,9 @@ impl<'a> WindowContext<'a> {
     }
 
     /// Mark the window as dirty, scheduling it to be redrawn on the next frame.
-    pub fn notify(&mut self) {
+    pub fn refresh(&mut self) {
         if !self.window.drawing {
+            self.window.refreshing = true;
             self.window.dirty = true;
         }
     }
@@ -525,7 +591,7 @@ impl<'a> WindowContext<'a> {
             self.window.focus_invalidated = true;
         }
 
-        self.notify();
+        self.refresh();
     }
 
     /// Remove focus from all elements within this context's window.
@@ -535,7 +601,7 @@ impl<'a> WindowContext<'a> {
         }
 
         self.window.focus = None;
-        self.notify();
+        self.refresh();
     }
 
     /// Blur the window and don't allow anything in it to be focused again.
@@ -772,6 +838,14 @@ impl<'a> WindowContext<'a> {
             .request_measured_layout(style, rem_size, measure)
     }
 
+    pub(crate) fn layout_style(&self, layout_id: LayoutId) -> Option<&Style> {
+        self.window
+            .layout_engine
+            .as_ref()
+            .unwrap()
+            .requested_style(layout_id)
+    }
+
     /// Compute the layout for the given id within the given available space.
     /// This method is called for its side effect, typically by the framework prior to painting.
     /// After calling it, you can request the bounds of the given layout node id or any descendant.
@@ -801,7 +875,7 @@ impl<'a> WindowContext<'a> {
         self.window.viewport_size = self.window.platform_window.content_size();
         self.window.bounds = self.window.platform_window.bounds();
         self.window.display_id = self.window.platform_window.display().id();
-        self.notify();
+        self.refresh();
 
         self.window
             .bounds_observers
@@ -898,22 +972,22 @@ impl<'a> WindowContext<'a> {
         &mut self,
         mut handler: impl FnMut(&Event, DispatchPhase, &mut WindowContext) + 'static,
     ) {
+        let view_id = self.parent_view_id();
         let order = self.window.next_frame.z_index_stack.clone();
-        let handler = self
-            .window
-            .frame_arena
-            .alloc(|| {
-                move |event: &dyn Any, phase: DispatchPhase, cx: &mut WindowContext<'_>| {
-                    handler(event.downcast_ref().unwrap(), phase, cx)
-                }
-            })
-            .map(|handler| handler as _);
         self.window
             .next_frame
             .mouse_listeners
             .entry(TypeId::of::<Event>())
             .or_default()
-            .push((order, handler))
+            .push((
+                order,
+                view_id,
+                Box::new(
+                    move |event: &dyn Any, phase: DispatchPhase, cx: &mut WindowContext<'_>| {
+                        handler(event.downcast_ref().unwrap(), phase, cx)
+                    },
+                ),
+            ))
     }
 
     /// Register a key event listener on the window for the next frame. The type of event
@@ -926,21 +1000,13 @@ impl<'a> WindowContext<'a> {
         &mut self,
         listener: impl Fn(&Event, DispatchPhase, &mut WindowContext) + 'static,
     ) {
-        let listener = self
-            .window
-            .frame_arena
-            .alloc(|| {
-                move |event: &dyn Any, phase, cx: &mut WindowContext<'_>| {
-                    if let Some(event) = event.downcast_ref::<Event>() {
-                        listener(event, phase, cx)
-                    }
+        self.window.next_frame.dispatch_tree.on_key_event(Rc::new(
+            move |event: &dyn Any, phase, cx: &mut WindowContext<'_>| {
+                if let Some(event) = event.downcast_ref::<Event>() {
+                    listener(event, phase, cx)
                 }
-            })
-            .map(|handler| handler as _);
-        self.window
-            .next_frame
-            .dispatch_tree
-            .on_key_event(ArenaRef::from(listener));
+            },
+        ));
     }
 
     /// Register an action listener on the window for the next frame. The type of action
@@ -954,15 +1020,10 @@ impl<'a> WindowContext<'a> {
         action_type: TypeId,
         listener: impl Fn(&dyn Any, DispatchPhase, &mut WindowContext) + 'static,
     ) {
-        let listener = self
-            .window
-            .frame_arena
-            .alloc(|| listener)
-            .map(|handler| handler as _);
         self.window
             .next_frame
             .dispatch_tree
-            .on_action(action_type, ArenaRef::from(listener));
+            .on_action(action_type, Rc::new(listener));
     }
 
     /// Determine whether the given action is available along the dispatch path to the currently focused element.
@@ -994,15 +1055,24 @@ impl<'a> WindowContext<'a> {
 
     /// Update the cursor style at the platform level.
     pub fn set_cursor_style(&mut self, style: CursorStyle) {
-        self.window.requested_cursor_style = Some(style)
+        let view_id = self.parent_view_id();
+        self.window.next_frame.cursor_styles.insert(view_id, style);
+        self.window.next_frame.requested_cursor_style = Some(style);
+    }
+
+    /// Set a tooltip to be rendered for the upcoming frame
+    pub fn set_tooltip(&mut self, tooltip: AnyTooltip) {
+        let view_id = self.parent_view_id();
+        self.window.next_frame.tooltip_request = Some(TooltipRequest { view_id, tooltip });
     }
 
     /// Called during painting to track which z-index is on top at each pixel position
     pub fn add_opaque_layer(&mut self, bounds: Bounds<Pixels>) {
         let stacking_order = self.window.next_frame.z_index_stack.clone();
+        let view_id = self.parent_view_id();
         let depth_map = &mut self.window.next_frame.depth_map;
-        match depth_map.binary_search_by(|(level, _)| stacking_order.cmp(level)) {
-            Ok(i) | Err(i) => depth_map.insert(i, (stacking_order, bounds)),
+        match depth_map.binary_search_by(|(level, _, _)| stacking_order.cmp(level)) {
+            Ok(i) | Err(i) => depth_map.insert(i, (stacking_order, view_id, bounds)),
         }
     }
 
@@ -1010,7 +1080,7 @@ impl<'a> WindowContext<'a> {
     /// on top of the given level. Layers whose level is an extension of the
     /// level are not considered to be on top of the level.
     pub fn was_top_layer(&self, point: &Point<Pixels>, level: &StackingOrder) -> bool {
-        for (opaque_level, bounds) in self.window.rendered_frame.depth_map.iter() {
+        for (opaque_level, _, bounds) in self.window.rendered_frame.depth_map.iter() {
             if level >= opaque_level {
                 break;
             }
@@ -1027,7 +1097,7 @@ impl<'a> WindowContext<'a> {
         point: &Point<Pixels>,
         level: &StackingOrder,
     ) -> bool {
-        for (opaque_level, bounds) in self.window.rendered_frame.depth_map.iter() {
+        for (opaque_level, _, bounds) in self.window.rendered_frame.depth_map.iter() {
             if level >= opaque_level {
                 break;
             }
@@ -1056,14 +1126,17 @@ impl<'a> WindowContext<'a> {
     ) {
         let scale_factor = self.scale_factor();
         let content_mask = self.content_mask();
+        let view_id = self.parent_view_id();
         let window = &mut *self.window;
         for shadow in shadows {
             let mut shadow_bounds = bounds;
             shadow_bounds.origin += shadow.offset;
             shadow_bounds.dilate(shadow.spread_radius);
-            window.next_frame.scene_builder.insert(
+            window.next_frame.scene.insert(
                 &window.next_frame.z_index_stack,
                 Shadow {
+                    view_id: view_id.into(),
+                    layer_id: 0,
                     order: 0,
                     bounds: shadow_bounds.scale(scale_factor),
                     content_mask: content_mask.scale(scale_factor),
@@ -1081,11 +1154,14 @@ impl<'a> WindowContext<'a> {
     pub fn paint_quad(&mut self, quad: PaintQuad) {
         let scale_factor = self.scale_factor();
         let content_mask = self.content_mask();
+        let view_id = self.parent_view_id();
 
         let window = &mut *self.window;
-        window.next_frame.scene_builder.insert(
+        window.next_frame.scene.insert(
             &window.next_frame.z_index_stack,
             Quad {
+                view_id: view_id.into(),
+                layer_id: 0,
                 order: 0,
                 bounds: quad.bounds.scale(scale_factor),
                 content_mask: content_mask.scale(scale_factor),
@@ -1101,12 +1177,15 @@ impl<'a> WindowContext<'a> {
     pub fn paint_path(&mut self, mut path: Path<Pixels>, color: impl Into<Hsla>) {
         let scale_factor = self.scale_factor();
         let content_mask = self.content_mask();
+        let view_id = self.parent_view_id();
+
         path.content_mask = content_mask;
         path.color = color.into();
+        path.view_id = view_id.into();
         let window = &mut *self.window;
         window
             .next_frame
-            .scene_builder
+            .scene
             .insert(&window.next_frame.z_index_stack, path.scale(scale_factor));
     }
 
@@ -1128,10 +1207,14 @@ impl<'a> WindowContext<'a> {
             size: size(width, height),
         };
         let content_mask = self.content_mask();
+        let view_id = self.parent_view_id();
+
         let window = &mut *self.window;
-        window.next_frame.scene_builder.insert(
+        window.next_frame.scene.insert(
             &window.next_frame.z_index_stack,
             Underline {
+                view_id: view_id.into(),
+                layer_id: 0,
                 order: 0,
                 bounds: bounds.scale(scale_factor),
                 content_mask: content_mask.scale(scale_factor),
@@ -1181,10 +1264,13 @@ impl<'a> WindowContext<'a> {
                 size: tile.bounds.size.map(Into::into),
             };
             let content_mask = self.content_mask().scale(scale_factor);
+            let view_id = self.parent_view_id();
             let window = &mut *self.window;
-            window.next_frame.scene_builder.insert(
+            window.next_frame.scene.insert(
                 &window.next_frame.z_index_stack,
                 MonochromeSprite {
+                    view_id: view_id.into(),
+                    layer_id: 0,
                     order: 0,
                     bounds,
                     content_mask,
@@ -1231,11 +1317,14 @@ impl<'a> WindowContext<'a> {
                 size: tile.bounds.size.map(Into::into),
             };
             let content_mask = self.content_mask().scale(scale_factor);
+            let view_id = self.parent_view_id();
             let window = &mut *self.window;
 
-            window.next_frame.scene_builder.insert(
+            window.next_frame.scene.insert(
                 &window.next_frame.z_index_stack,
                 PolychromeSprite {
+                    view_id: view_id.into(),
+                    layer_id: 0,
                     order: 0,
                     bounds,
                     corner_radii: Default::default(),
@@ -1273,11 +1362,14 @@ impl<'a> WindowContext<'a> {
                     Ok((params.size, Cow::Owned(bytes)))
                 })?;
         let content_mask = self.content_mask().scale(scale_factor);
+        let view_id = self.parent_view_id();
 
         let window = &mut *self.window;
-        window.next_frame.scene_builder.insert(
+        window.next_frame.scene.insert(
             &window.next_frame.z_index_stack,
             MonochromeSprite {
+                view_id: view_id.into(),
+                layer_id: 0,
                 order: 0,
                 bounds,
                 content_mask,
@@ -1309,11 +1401,14 @@ impl<'a> WindowContext<'a> {
             })?;
         let content_mask = self.content_mask().scale(scale_factor);
         let corner_radii = corner_radii.scale(scale_factor);
+        let view_id = self.parent_view_id();
 
         let window = &mut *self.window;
-        window.next_frame.scene_builder.insert(
+        window.next_frame.scene.insert(
             &window.next_frame.z_index_stack,
             PolychromeSprite {
+                view_id: view_id.into(),
+                layer_id: 0,
                 order: 0,
                 bounds,
                 content_mask,
@@ -1330,10 +1425,13 @@ impl<'a> WindowContext<'a> {
         let scale_factor = self.scale_factor();
         let bounds = bounds.scale(scale_factor);
         let content_mask = self.content_mask().scale(scale_factor);
+        let view_id = self.parent_view_id();
         let window = &mut *self.window;
-        window.next_frame.scene_builder.insert(
+        window.next_frame.scene.insert(
             &window.next_frame.z_index_stack,
             Surface {
+                view_id: view_id.into(),
+                layer_id: 0,
                 order: 0,
                 bounds,
                 content_mask,
@@ -1342,8 +1440,50 @@ impl<'a> WindowContext<'a> {
         );
     }
 
+    pub(crate) fn reuse_view(&mut self) {
+        let view_id = self.parent_view_id();
+        let grafted_view_ids = self
+            .window
+            .next_frame
+            .dispatch_tree
+            .reuse_view(view_id, &mut self.window.rendered_frame.dispatch_tree);
+        for view_id in grafted_view_ids {
+            assert!(self.window.next_frame.reused_views.insert(view_id));
+
+            // Reuse the previous input handler requested during painting of the reused view.
+            if self
+                .window
+                .rendered_frame
+                .requested_input_handler
+                .as_ref()
+                .map_or(false, |requested| requested.view_id == view_id)
+            {
+                self.window.next_frame.requested_input_handler =
+                    self.window.rendered_frame.requested_input_handler.take();
+            }
+
+            // Reuse the tooltip previously requested during painting of the reused view.
+            if self
+                .window
+                .rendered_frame
+                .tooltip_request
+                .as_ref()
+                .map_or(false, |requested| requested.view_id == view_id)
+            {
+                self.window.next_frame.tooltip_request =
+                    self.window.rendered_frame.tooltip_request.take();
+            }
+
+            // Reuse the cursor styles previously requested during painting of the reused view.
+            if let Some(style) = self.window.rendered_frame.cursor_styles.remove(&view_id) {
+                self.window.next_frame.cursor_styles.insert(view_id, style);
+                self.window.next_frame.requested_cursor_style = Some(style);
+            }
+        }
+    }
+
     /// Draw pixels to the display for this window based on the contents of its scene.
-    pub(crate) fn draw(&mut self) -> Scene {
+    pub(crate) fn draw(&mut self) {
         self.window.dirty = false;
         self.window.drawing = true;
 
@@ -1352,30 +1492,23 @@ impl<'a> WindowContext<'a> {
             self.window.focus_invalidated = false;
         }
 
-        self.text_system().start_frame();
-        self.window.platform_window.clear_input_handler();
-        self.window.layout_engine.as_mut().unwrap().clear();
-        self.window.next_frame.clear();
-        self.window.frame_arena.clear();
+        if let Some(requested_handler) = self.window.rendered_frame.requested_input_handler.as_mut()
+        {
+            requested_handler.handler = self.window.platform_window.take_input_handler();
+        }
+
         let root_view = self.window.root_view.take().unwrap();
 
         self.with_z_index(0, |cx| {
             cx.with_key_dispatch(Some(KeyContext::default()), None, |_, cx| {
                 for (action_type, action_listeners) in &cx.app.global_action_listeners {
                     for action_listener in action_listeners.iter().cloned() {
-                        let listener = cx
-                            .window
-                            .frame_arena
-                            .alloc(|| {
-                                move |action: &dyn Any, phase, cx: &mut WindowContext<'_>| {
-                                    action_listener(action, phase, cx)
-                                }
-                            })
-                            .map(|listener| listener as _);
-                        cx.window
-                            .next_frame
-                            .dispatch_tree
-                            .on_action(*action_type, ArenaRef::from(listener))
+                        cx.window.next_frame.dispatch_tree.on_action(
+                            *action_type,
+                            Rc::new(move |action: &dyn Any, phase, cx: &mut WindowContext<'_>| {
+                                action_listener(action, phase, cx)
+                            }),
+                        )
                     }
                 }
 
@@ -1391,14 +1524,18 @@ impl<'a> WindowContext<'a> {
                 active_drag.view.draw(offset, available_space, cx);
             });
             self.active_drag = Some(active_drag);
-        } else if let Some(active_tooltip) = self.app.active_tooltip.take() {
+        } else if let Some(tooltip_request) = self.window.next_frame.tooltip_request.take() {
             self.with_z_index(1, |cx| {
                 let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent);
-                active_tooltip
-                    .view
-                    .draw(active_tooltip.cursor_offset, available_space, cx);
+                tooltip_request.tooltip.view.draw(
+                    tooltip_request.tooltip.cursor_offset,
+                    available_space,
+                    cx,
+                );
             });
+            self.window.next_frame.tooltip_request = Some(tooltip_request);
         }
+        self.window.dirty_views.clear();
 
         self.window
             .next_frame
@@ -1411,17 +1548,10 @@ impl<'a> WindowContext<'a> {
         self.window.next_frame.window_active = self.window.active;
         self.window.root_view = Some(root_view);
 
-        let previous_focus_path = self.window.rendered_frame.focus_path();
-        let previous_window_active = self.window.rendered_frame.window_active;
-        mem::swap(&mut self.window.rendered_frame, &mut self.window.next_frame);
-        let current_focus_path = self.window.rendered_frame.focus_path();
-        let current_window_active = self.window.rendered_frame.window_active;
-
-        let scene = self.window.rendered_frame.scene_builder.build();
-
         // Set the cursor only if we're the active window.
         let cursor_style = self
             .window
+            .next_frame
             .requested_cursor_style
             .take()
             .unwrap_or(CursorStyle::Arrow);
@@ -1429,6 +1559,28 @@ impl<'a> WindowContext<'a> {
             self.platform.set_cursor_style(cursor_style);
         }
 
+        // Register requested input handler with the platform window.
+        if let Some(requested_input) = self.window.next_frame.requested_input_handler.as_mut() {
+            if let Some(handler) = requested_input.handler.take() {
+                self.window.platform_window.set_input_handler(handler);
+            }
+        }
+
+        self.window.layout_engine.as_mut().unwrap().clear();
+        self.text_system()
+            .finish_frame(&self.window.next_frame.reused_views);
+        self.window
+            .next_frame
+            .finish(&mut self.window.rendered_frame);
+        ELEMENT_ARENA.with_borrow_mut(|element_arena| element_arena.clear());
+
+        let previous_focus_path = self.window.rendered_frame.focus_path();
+        let previous_window_active = self.window.rendered_frame.window_active;
+        mem::swap(&mut self.window.rendered_frame, &mut self.window.next_frame);
+        self.window.next_frame.clear();
+        let current_focus_path = self.window.rendered_frame.focus_path();
+        let current_window_active = self.window.rendered_frame.window_active;
+
         if previous_focus_path != current_focus_path
             || previous_window_active != current_window_active
         {
@@ -1457,10 +1609,11 @@ impl<'a> WindowContext<'a> {
                 .retain(&(), |listener| listener(&event, self));
         }
 
+        self.window
+            .platform_window
+            .draw(&self.window.rendered_frame.scene);
+        self.window.refreshing = false;
         self.window.drawing = false;
-        ELEMENT_ARENA.with_borrow_mut(|element_arena| element_arena.clear());
-
-        scene
     }
 
     /// Dispatch a mouse or keyboard event on the window.
@@ -1564,11 +1717,11 @@ impl<'a> WindowContext<'a> {
             .remove(&event.type_id())
         {
             // Because handlers may add other handlers, we sort every time.
-            handlers.sort_by(|(a, _), (b, _)| a.cmp(b));
+            handlers.sort_by(|(a, _, _), (b, _, _)| a.cmp(b));
 
             // Capture phase, events bubble from back to front. Handlers for this phase are used for
             // special purposes, such as detecting events outside of a given Bounds.
-            for (_, handler) in &mut handlers {
+            for (_, _, handler) in &mut handlers {
                 handler(event, DispatchPhase::Capture, self);
                 if !self.app.propagate_event {
                     break;
@@ -1577,7 +1730,7 @@ impl<'a> WindowContext<'a> {
 
             // Bubble phase, where most normal handlers do their work.
             if self.app.propagate_event {
-                for (_, handler) in handlers.iter_mut().rev() {
+                for (_, _, handler) in handlers.iter_mut().rev() {
                     handler(event, DispatchPhase::Bubble, self);
                     if !self.app.propagate_event {
                         break;
@@ -1595,12 +1748,12 @@ impl<'a> WindowContext<'a> {
             if event.is::<MouseMoveEvent>() {
                 // If this was a mouse move event, redraw the window so that the
                 // active drag can follow the mouse cursor.
-                self.notify();
+                self.refresh();
             } else if event.is::<MouseUpEvent>() {
                 // If this was a mouse up event, cancel the active drag and redraw
                 // the window.
                 self.active_drag = None;
-                self.notify();
+                self.refresh();
             }
         }
     }
@@ -1867,13 +2020,12 @@ impl<'a> WindowContext<'a> {
         f: impl FnOnce(Option<FocusHandle>, &mut Self) -> R,
     ) -> R {
         let window = &mut self.window;
-        window.next_frame.dispatch_tree.push_node(context.clone());
-        if let Some(focus_handle) = focus_handle.as_ref() {
-            window
-                .next_frame
-                .dispatch_tree
-                .make_focusable(focus_handle.id);
-        }
+        let focus_id = focus_handle.as_ref().map(|handle| handle.id);
+        window
+            .next_frame
+            .dispatch_tree
+            .push_node(context.clone(), focus_id, None);
+
         let result = f(focus_handle, self);
 
         self.window.next_frame.dispatch_tree.pop_node();
@@ -1881,9 +2033,149 @@ impl<'a> WindowContext<'a> {
         result
     }
 
+    /// Invoke the given function with the given view id present on the view stack.
+    /// This is a fairly low-level method used to layout views.
+    pub fn with_view_id<R>(&mut self, view_id: EntityId, f: impl FnOnce(&mut Self) -> R) -> R {
+        let text_system = self.text_system().clone();
+        text_system.with_view(view_id, || {
+            if self.window.next_frame.view_stack.last() == Some(&view_id) {
+                return f(self);
+            } else {
+                self.window.next_frame.view_stack.push(view_id);
+                let result = f(self);
+                self.window.next_frame.view_stack.pop();
+                result
+            }
+        })
+    }
+
+    /// Invoke the given function with the given view id present on the view stack.
+    /// This is a fairly low-level method used to paint views.
+    pub fn paint_view<R>(&mut self, view_id: EntityId, f: impl FnOnce(&mut Self) -> R) -> R {
+        let text_system = self.text_system().clone();
+        text_system.with_view(view_id, || {
+            if self.window.next_frame.view_stack.last() == Some(&view_id) {
+                return f(self);
+            } else {
+                self.window.next_frame.view_stack.push(view_id);
+                self.window
+                    .next_frame
+                    .dispatch_tree
+                    .push_node(None, None, Some(view_id));
+                let result = f(self);
+                self.window.next_frame.dispatch_tree.pop_node();
+                self.window.next_frame.view_stack.pop();
+                result
+            }
+        })
+    }
+
+    /// Update or initialize state for an element with the given id that lives across multiple
+    /// frames. If an element with this id existed in the rendered frame, its state will be passed
+    /// to the given closure. The state returned by the closure will be stored so it can be referenced
+    /// when drawing the next frame.
+    pub(crate) fn with_element_state<S, R>(
+        &mut self,
+        id: ElementId,
+        f: impl FnOnce(Option<S>, &mut Self) -> (R, S),
+    ) -> R
+    where
+        S: 'static,
+    {
+        self.with_element_id(Some(id), |cx| {
+            let global_id = cx.window().element_id_stack.clone();
+
+            if let Some(any) = cx
+                .window_mut()
+                .next_frame
+                .element_states
+                .remove(&global_id)
+                .or_else(|| {
+                    cx.window_mut()
+                        .rendered_frame
+                        .element_states
+                        .remove(&global_id)
+                })
+            {
+                let ElementStateBox {
+                    inner,
+                    parent_view_id,
+                    #[cfg(debug_assertions)]
+                    type_name
+                } = any;
+                // Using the extra inner option to avoid needing to reallocate a new box.
+                let mut state_box = inner
+                    .downcast::<Option<S>>()
+                    .map_err(|_| {
+                        #[cfg(debug_assertions)]
+                        {
+                            anyhow!(
+                                "invalid element state type for id, requested_type {:?}, actual type: {:?}",
+                                std::any::type_name::<S>(),
+                                type_name
+                            )
+                        }
+
+                        #[cfg(not(debug_assertions))]
+                        {
+                            anyhow!(
+                                "invalid element state type for id, requested_type {:?}",
+                                std::any::type_name::<S>(),
+                            )
+                        }
+                    })
+                    .unwrap();
+
+                // Actual: Option<AnyElement> <- View
+                // Requested: () <- AnyElemet
+                let state = state_box
+                    .take()
+                    .expect("element state is already on the stack");
+                let (result, state) = f(Some(state), cx);
+                state_box.replace(state);
+                cx.window_mut()
+                    .next_frame
+                    .element_states
+                    .insert(global_id, ElementStateBox {
+                        inner: state_box,
+                        parent_view_id,
+                        #[cfg(debug_assertions)]
+                        type_name
+                    });
+                result
+            } else {
+                let (result, state) = f(None, cx);
+                let parent_view_id = cx.parent_view_id();
+                cx.window_mut()
+                    .next_frame
+                    .element_states
+                    .insert(global_id,
+                        ElementStateBox {
+                            inner: Box::new(Some(state)),
+                            parent_view_id,
+                            #[cfg(debug_assertions)]
+                            type_name: std::any::type_name::<S>()
+                        }
+
+                    );
+                result
+            }
+        })
+    }
+
+    fn parent_view_id(&self) -> EntityId {
+        *self
+            .window
+            .next_frame
+            .view_stack
+            .last()
+            .expect("a view should always be on the stack while drawing")
+    }
+
     /// Set an input handler, such as [`ElementInputHandler`][element_input_handler], which interfaces with the
     /// platform to receive textual input with proper integration with concerns such
-    /// as IME interactions.
+    /// as IME interactions. This handler will be active for the upcoming frame until the following frame is
+    /// rendered.
     ///
     /// [element_input_handler]: crate::ElementInputHandler
     pub fn handle_input(
@@ -1892,9 +2184,11 @@ impl<'a> WindowContext<'a> {
         input_handler: impl PlatformInputHandler,
     ) {
         if focus_handle.is_focused(self) {
-            self.window
-                .platform_window
-                .set_input_handler(Box::new(input_handler));
+            let view_id = self.parent_view_id();
+            self.window.next_frame.requested_input_handler = Some(RequestedInputHandler {
+                view_id,
+                handler: Some(Box::new(input_handler)),
+            })
         }
     }
 
@@ -2040,7 +2334,7 @@ impl VisualContext for WindowContext<'_> {
     {
         let view = self.new_view(build_view);
         self.window.root_view = Some(view.clone().into());
-        self.notify();
+        self.refresh();
         view
     }
 
@@ -2223,98 +2517,6 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
             .unwrap_or_default()
     }
 
-    /// Update or initialize state for an element with the given id that lives across multiple
-    /// frames. If an element with this id existed in the rendered frame, its state will be passed
-    /// to the given closure. The state returned by the closure will be stored so it can be referenced
-    /// when drawing the next frame.
-    fn with_element_state<S, R>(
-        &mut self,
-        id: ElementId,
-        f: impl FnOnce(Option<S>, &mut Self) -> (R, S),
-    ) -> R
-    where
-        S: 'static,
-    {
-        self.with_element_id(Some(id), |cx| {
-            let global_id = cx.window().element_id_stack.clone();
-
-            if let Some(any) = cx
-                .window_mut()
-                .next_frame
-                .element_states
-                .remove(&global_id)
-                .or_else(|| {
-                    cx.window_mut()
-                        .rendered_frame
-                        .element_states
-                        .remove(&global_id)
-                })
-            {
-                let ElementStateBox {
-                    inner,
-
-                    #[cfg(debug_assertions)]
-                    type_name
-                } = any;
-                // Using the extra inner option to avoid needing to reallocate a new box.
-                let mut state_box = inner
-                    .downcast::<Option<S>>()
-                    .map_err(|_| {
-                        #[cfg(debug_assertions)]
-                        {
-                            anyhow!(
-                                "invalid element state type for id, requested_type {:?}, actual type: {:?}",
-                                std::any::type_name::<S>(),
-                                type_name
-                            )
-                        }
-
-                        #[cfg(not(debug_assertions))]
-                        {
-                            anyhow!(
-                                "invalid element state type for id, requested_type {:?}",
-                                std::any::type_name::<S>(),
-                            )
-                        }
-                    })
-                    .unwrap();
-
-                // Actual: Option<AnyElement> <- View
-                // Requested: () <- AnyElemet
-                let state = state_box
-                    .take()
-                    .expect("element state is already on the stack");
-                let (result, state) = f(Some(state), cx);
-                state_box.replace(state);
-                cx.window_mut()
-                    .next_frame
-                    .element_states
-                    .insert(global_id, ElementStateBox {
-                        inner: state_box,
-
-                        #[cfg(debug_assertions)]
-                        type_name
-                    });
-                result
-            } else {
-                let (result, state) = f(None, cx);
-                cx.window_mut()
-                    .next_frame
-                    .element_states
-                    .insert(global_id,
-                        ElementStateBox {
-                            inner: Box::new(Some(state)),
-
-                            #[cfg(debug_assertions)]
-                            type_name: std::any::type_name::<S>()
-                        }
-
-                    );
-                result
-            }
-        })
-    }
-
     /// Obtain the current content mask.
     fn content_mask(&self) -> ContentMask<Pixels> {
         self.window()

crates/gpui_macros/src/register_action.rs 🔗

@@ -1,16 +1,3 @@
-// Input:
-//
-// struct FooBar {}
-
-// Output:
-//
-// struct FooBar {}
-//
-// #[allow(non_snake_case)]
-// #[gpui2::ctor]
-// fn register_foobar_builder() {
-//     gpui2::register_action_builder::<Foo>()
-// }
 use proc_macro::TokenStream;
 use proc_macro2::Ident;
 use quote::{format_ident, quote};

crates/language_selector/src/language_selector.rs 🔗

@@ -68,7 +68,7 @@ impl LanguageSelector {
 
 impl Render for LanguageSelector {
     fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
-        v_stack().w(rems(34.)).child(self.picker.clone())
+        v_flex().w(rems(34.)).child(self.picker.clone())
     }
 }
 

crates/language_tools/src/lsp_log.rs 🔗

@@ -405,8 +405,14 @@ impl LspLogView {
                     {
                         log_view.editor.update(cx, |editor, cx| {
                             editor.set_read_only(false);
-                            editor.handle_input(entry.trim(), cx);
-                            editor.handle_input("\n", cx);
+                            let last_point = editor.buffer().read(cx).len(cx);
+                            editor.edit(
+                                vec![
+                                    (last_point..last_point, entry.trim()),
+                                    (last_point..last_point, "\n"),
+                                ],
+                                cx,
+                            );
                             editor.set_read_only(true);
                         });
                     }
@@ -449,6 +455,7 @@ impl LspLogView {
             editor.set_text(log_contents, cx);
             editor.move_to_end(&MoveToEnd, cx);
             editor.set_read_only(true);
+            editor.set_show_copilot_suggestions(false);
             editor
         });
         let editor_subscription = cx.subscribe(
@@ -624,6 +631,10 @@ impl Item for LspLogView {
             .into_any_element()
     }
 
+    fn telemetry_event_text(&self) -> Option<&'static str> {
+        None
+    }
+
     fn as_searchable(&self, handle: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
         Some(Box::new(handle.clone()))
     }
@@ -784,7 +795,7 @@ impl Render for LspLogToolbarItemView {
                             {
                                 let log_toolbar_view = log_toolbar_view.clone();
                                 move |cx| {
-                                    h_stack()
+                                    h_flex()
                                         .w_full()
                                         .justify_between()
                                         .child(Label::new(RPC_MESSAGES))
@@ -836,7 +847,7 @@ impl Render for LspLogToolbarItemView {
                 .into()
             });
 
-        h_stack().size_full().child(lsp_menu).child(
+        h_flex().size_full().child(lsp_menu).child(
             div()
                 .child(
                     Button::new("clear_log_button", "Clear").on_click(cx.listener(

crates/language_tools/src/syntax_tree_view.rs 🔗

@@ -9,7 +9,7 @@ use language::{Buffer, OwnedSyntaxLayerInfo};
 use std::{mem, ops::Range};
 use theme::ActiveTheme;
 use tree_sitter::{Node, TreeCursor};
-use ui::{h_stack, popover_menu, ButtonLike, Color, ContextMenu, Label, LabelCommon, PopoverMenu};
+use ui::{h_flex, popover_menu, ButtonLike, Color, ContextMenu, Label, LabelCommon, PopoverMenu};
 use workspace::{
     item::{Item, ItemHandle},
     SplitDirection, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
@@ -239,7 +239,7 @@ impl SyntaxTreeView {
 
     fn render_node(cursor: &TreeCursor, depth: u32, selected: bool, cx: &AppContext) -> Div {
         let colors = cx.theme().colors();
-        let mut row = h_stack();
+        let mut row = h_flex();
         if let Some(field_name) = cursor.field_name() {
             row = row.children([Label::new(field_name).color(Color::Info), Label::new(": ")]);
         }
@@ -397,6 +397,10 @@ impl Item for SyntaxTreeView {
             .into_any_element()
     }
 
+    fn telemetry_event_text(&self) -> Option<&'static str> {
+        None
+    }
+
     fn clone_on_split(
         &self,
         _: workspace::WorkspaceId,

crates/live_kit_client/LiveKitBridge/Sources/LiveKitBridge/LiveKitBridge.swift 🔗

@@ -286,6 +286,18 @@ public func LKRemoteAudioTrackGetSid(track: UnsafeRawPointer) -> CFString {
     return track.sid! as CFString
 }
 
+@_cdecl("LKRemoteAudioTrackStart")
+public func LKRemoteAudioTrackStart(track: UnsafeRawPointer) {
+    let track = Unmanaged<RemoteAudioTrack>.fromOpaque(track).takeUnretainedValue()
+    track.start()
+}
+
+@_cdecl("LKRemoteAudioTrackStop")
+public func LKRemoteAudioTrackStop(track: UnsafeRawPointer) {
+    let track = Unmanaged<RemoteAudioTrack>.fromOpaque(track).takeUnretainedValue()
+    track.stop()
+}
+
 @_cdecl("LKDisplaySources")
 public func LKDisplaySources(data: UnsafeRawPointer, callback: @escaping @convention(c) (UnsafeRawPointer, CFArray?, CFString?) -> Void) {
     MacOSScreenCapturer.sources(for: .display, includeCurrentApplication: false, preferredMethod: .legacy).then { displaySources in

crates/live_kit_client/src/prod.rs 🔗

@@ -18,8 +18,6 @@ use std::{
     sync::{Arc, Weak},
 };
 
-// SAFETY: Most live kit types are threadsafe:
-// https://github.com/livekit/client-sdk-swift#thread-safety
 macro_rules! pointer_type {
     ($pointer_name:ident) => {
         #[repr(transparent)]
@@ -134,8 +132,10 @@ extern "C" {
     ) -> *const c_void;
 
     fn LKRemoteAudioTrackGetSid(track: swift::RemoteAudioTrack) -> CFStringRef;
-    fn LKVideoTrackAddRenderer(track: swift::RemoteVideoTrack, renderer: *const c_void);
     fn LKRemoteVideoTrackGetSid(track: swift::RemoteVideoTrack) -> CFStringRef;
+    fn LKRemoteAudioTrackStart(track: swift::RemoteAudioTrack);
+    fn LKRemoteAudioTrackStop(track: swift::RemoteAudioTrack);
+    fn LKVideoTrackAddRenderer(track: swift::RemoteVideoTrack, renderer: *const c_void);
 
     fn LKDisplaySources(
         callback_data: *mut c_void,
@@ -853,12 +853,12 @@ impl RemoteAudioTrack {
         &self.publisher_id
     }
 
-    pub fn enable(&self) -> impl Future<Output = Result<()>> {
-        async { Ok(()) }
+    pub fn start(&self) {
+        unsafe { LKRemoteAudioTrackStart(self.native_track) }
     }
 
-    pub fn disable(&self) -> impl Future<Output = Result<()>> {
-        async { Ok(()) }
+    pub fn stop(&self) {
+        unsafe { LKRemoteAudioTrackStop(self.native_track) }
     }
 }
 

crates/live_kit_client/src/test.rs 🔗

@@ -1,7 +1,7 @@
 use crate::{ConnectionState, RoomUpdate, Sid};
 use anyhow::{anyhow, Context, Result};
 use async_trait::async_trait;
-use collections::{BTreeMap, HashMap};
+use collections::{BTreeMap, HashMap, HashSet};
 use futures::Stream;
 use gpui::BackgroundExecutor;
 use live_kit_server::{proto, token};
@@ -13,7 +13,7 @@ use std::{
     mem,
     sync::{
         atomic::{AtomicBool, Ordering::SeqCst},
-        Arc,
+        Arc, Weak,
     },
 };
 
@@ -113,7 +113,25 @@ impl TestServer {
                     .0
                     .lock()
                     .updates_tx
-                    .try_broadcast(RoomUpdate::SubscribedToRemoteVideoTrack(track.clone()))
+                    .try_broadcast(RoomUpdate::SubscribedToRemoteVideoTrack(Arc::new(
+                        RemoteVideoTrack {
+                            server_track: track.clone(),
+                        },
+                    )))
+                    .unwrap();
+            }
+            for track in &room.audio_tracks {
+                client_room
+                    .0
+                    .lock()
+                    .updates_tx
+                    .try_broadcast(RoomUpdate::SubscribedToRemoteAudioTrack(
+                        Arc::new(RemoteAudioTrack {
+                            server_track: track.clone(),
+                            room: Arc::downgrade(&client_room),
+                        }),
+                        Arc::new(RemoteTrackPublication),
+                    ))
                     .unwrap();
             }
             room.client_rooms.insert(identity, client_room);
@@ -210,7 +228,7 @@ impl TestServer {
         }
 
         let sid = nanoid::nanoid!(17);
-        let track = Arc::new(RemoteVideoTrack {
+        let track = Arc::new(TestServerVideoTrack {
             sid: sid.clone(),
             publisher_id: identity.clone(),
             frames_rx: local_track.frames_rx.clone(),
@@ -224,7 +242,11 @@ impl TestServer {
                     .0
                     .lock()
                     .updates_tx
-                    .try_broadcast(RoomUpdate::SubscribedToRemoteVideoTrack(track.clone()))
+                    .try_broadcast(RoomUpdate::SubscribedToRemoteVideoTrack(Arc::new(
+                        RemoteVideoTrack {
+                            server_track: track.clone(),
+                        },
+                    )))
                     .unwrap();
             }
         }
@@ -259,9 +281,10 @@ impl TestServer {
         }
 
         let sid = nanoid::nanoid!(17);
-        let track = Arc::new(RemoteAudioTrack {
+        let track = Arc::new(TestServerAudioTrack {
             sid: sid.clone(),
             publisher_id: identity.clone(),
+            muted: AtomicBool::new(false),
         });
 
         let publication = Arc::new(RemoteTrackPublication);
@@ -275,7 +298,10 @@ impl TestServer {
                     .lock()
                     .updates_tx
                     .try_broadcast(RoomUpdate::SubscribedToRemoteAudioTrack(
-                        track.clone(),
+                        Arc::new(RemoteAudioTrack {
+                            server_track: track.clone(),
+                            room: Arc::downgrade(&client_room),
+                        }),
                         publication.clone(),
                     ))
                     .unwrap();
@@ -285,37 +311,123 @@ impl TestServer {
         Ok(sid)
     }
 
+    fn set_track_muted(&self, token: &str, track_sid: &str, muted: bool) -> Result<()> {
+        let claims = live_kit_server::token::validate(&token, &self.secret_key)?;
+        let room_name = claims.video.room.unwrap();
+        let identity = claims.sub.unwrap();
+        let mut server_rooms = self.rooms.lock();
+        let room = server_rooms
+            .get_mut(&*room_name)
+            .ok_or_else(|| anyhow!("room {} does not exist", room_name))?;
+        if let Some(track) = room
+            .audio_tracks
+            .iter_mut()
+            .find(|track| track.sid == track_sid)
+        {
+            track.muted.store(muted, SeqCst);
+            for (id, client_room) in room.client_rooms.iter() {
+                if *id != identity {
+                    client_room
+                        .0
+                        .lock()
+                        .updates_tx
+                        .try_broadcast(RoomUpdate::RemoteAudioTrackMuteChanged {
+                            track_id: track_sid.to_string(),
+                            muted,
+                        })
+                        .unwrap();
+                }
+            }
+        }
+        Ok(())
+    }
+
+    fn is_track_muted(&self, token: &str, track_sid: &str) -> Option<bool> {
+        let claims = live_kit_server::token::validate(&token, &self.secret_key).ok()?;
+        let room_name = claims.video.room.unwrap();
+
+        let mut server_rooms = self.rooms.lock();
+        let room = server_rooms.get_mut(&*room_name)?;
+        room.audio_tracks.iter().find_map(|track| {
+            if track.sid == track_sid {
+                Some(track.muted.load(SeqCst))
+            } else {
+                None
+            }
+        })
+    }
+
     fn video_tracks(&self, token: String) -> Result<Vec<Arc<RemoteVideoTrack>>> {
         let claims = live_kit_server::token::validate(&token, &self.secret_key)?;
         let room_name = claims.video.room.unwrap();
+        let identity = claims.sub.unwrap();
 
         let mut server_rooms = self.rooms.lock();
         let room = server_rooms
             .get_mut(&*room_name)
             .ok_or_else(|| anyhow!("room {} does not exist", room_name))?;
-        Ok(room.video_tracks.clone())
+        room.client_rooms
+            .get(identity.as_ref())
+            .ok_or_else(|| anyhow!("not a participant in room"))?;
+        Ok(room
+            .video_tracks
+            .iter()
+            .map(|track| {
+                Arc::new(RemoteVideoTrack {
+                    server_track: track.clone(),
+                })
+            })
+            .collect())
     }
 
     fn audio_tracks(&self, token: String) -> Result<Vec<Arc<RemoteAudioTrack>>> {
         let claims = live_kit_server::token::validate(&token, &self.secret_key)?;
         let room_name = claims.video.room.unwrap();
+        let identity = claims.sub.unwrap();
 
         let mut server_rooms = self.rooms.lock();
         let room = server_rooms
             .get_mut(&*room_name)
             .ok_or_else(|| anyhow!("room {} does not exist", room_name))?;
-        Ok(room.audio_tracks.clone())
+        let client_room = room
+            .client_rooms
+            .get(identity.as_ref())
+            .ok_or_else(|| anyhow!("not a participant in room"))?;
+        Ok(room
+            .audio_tracks
+            .iter()
+            .map(|track| {
+                Arc::new(RemoteAudioTrack {
+                    server_track: track.clone(),
+                    room: Arc::downgrade(&client_room),
+                })
+            })
+            .collect())
     }
 }
 
 #[derive(Default)]
 struct TestServerRoom {
     client_rooms: HashMap<Sid, Arc<Room>>,
-    video_tracks: Vec<Arc<RemoteVideoTrack>>,
-    audio_tracks: Vec<Arc<RemoteAudioTrack>>,
+    video_tracks: Vec<Arc<TestServerVideoTrack>>,
+    audio_tracks: Vec<Arc<TestServerAudioTrack>>,
     participant_permissions: HashMap<Sid, proto::ParticipantPermission>,
 }
 
+#[derive(Debug)]
+struct TestServerVideoTrack {
+    sid: Sid,
+    publisher_id: Sid,
+    frames_rx: async_broadcast::Receiver<Frame>,
+}
+
+#[derive(Debug)]
+struct TestServerAudioTrack {
+    sid: Sid,
+    publisher_id: Sid,
+    muted: AtomicBool,
+}
+
 impl TestServerRoom {}
 
 pub struct TestApiClient {
@@ -386,6 +498,7 @@ struct RoomState {
         watch::Receiver<ConnectionState>,
     ),
     display_sources: Vec<MacOSDisplay>,
+    paused_audio_tracks: HashSet<Sid>,
     updates_tx: async_broadcast::Sender<RoomUpdate>,
     updates_rx: async_broadcast::Receiver<RoomUpdate>,
 }
@@ -398,6 +511,7 @@ impl Room {
         Arc::new(Self(Mutex::new(RoomState {
             connection: watch::channel_with(ConnectionState::Disconnected),
             display_sources: Default::default(),
+            paused_audio_tracks: Default::default(),
             updates_tx,
             updates_rx,
         })))
@@ -443,11 +557,12 @@ impl Room {
                 .publish_video_track(this.token(), track)
                 .await?;
             Ok(LocalTrackPublication {
-                muted: Default::default(),
+                room: Arc::downgrade(&this),
                 sid,
             })
         }
     }
+
     pub fn publish_audio_track(
         self: &Arc<Self>,
         track: LocalAudioTrack,
@@ -460,7 +575,7 @@ impl Room {
                 .publish_audio_track(this.token(), &track)
                 .await?;
             Ok(LocalTrackPublication {
-                muted: Default::default(),
+                room: Arc::downgrade(&this),
                 sid,
             })
         }
@@ -560,20 +675,31 @@ impl Drop for Room {
 #[derive(Clone)]
 pub struct LocalTrackPublication {
     sid: String,
-    muted: Arc<AtomicBool>,
+    room: Weak<Room>,
 }
 
 impl LocalTrackPublication {
     pub fn set_mute(&self, mute: bool) -> impl Future<Output = Result<()>> {
-        let muted = self.muted.clone();
+        let sid = self.sid.clone();
+        let room = self.room.clone();
         async move {
-            muted.store(mute, SeqCst);
-            Ok(())
+            if let Some(room) = room.upgrade() {
+                room.test_server()
+                    .set_track_muted(&room.token(), &sid, mute)
+            } else {
+                Err(anyhow!("no such room"))
+            }
         }
     }
 
     pub fn is_muted(&self) -> bool {
-        self.muted.load(SeqCst)
+        if let Some(room) = self.room.upgrade() {
+            room.test_server()
+                .is_track_muted(&room.token(), &self.sid)
+                .unwrap_or(false)
+        } else {
+            false
+        }
     }
 
     pub fn sid(&self) -> String {
@@ -621,46 +747,65 @@ impl LocalAudioTrack {
 
 #[derive(Debug)]
 pub struct RemoteVideoTrack {
-    sid: Sid,
-    publisher_id: Sid,
-    frames_rx: async_broadcast::Receiver<Frame>,
+    server_track: Arc<TestServerVideoTrack>,
 }
 
 impl RemoteVideoTrack {
     pub fn sid(&self) -> &str {
-        &self.sid
+        &self.server_track.sid
     }
 
     pub fn publisher_id(&self) -> &str {
-        &self.publisher_id
+        &self.server_track.publisher_id
     }
 
     pub fn frames(&self) -> async_broadcast::Receiver<Frame> {
-        self.frames_rx.clone()
+        self.server_track.frames_rx.clone()
     }
 }
 
 #[derive(Debug)]
 pub struct RemoteAudioTrack {
-    sid: Sid,
-    publisher_id: Sid,
+    server_track: Arc<TestServerAudioTrack>,
+    room: Weak<Room>,
 }
 
 impl RemoteAudioTrack {
     pub fn sid(&self) -> &str {
-        &self.sid
+        &self.server_track.sid
     }
 
     pub fn publisher_id(&self) -> &str {
-        &self.publisher_id
+        &self.server_track.publisher_id
     }
 
-    pub fn enable(&self) -> impl Future<Output = Result<()>> {
-        async { Ok(()) }
+    pub fn start(&self) {
+        if let Some(room) = self.room.upgrade() {
+            room.0
+                .lock()
+                .paused_audio_tracks
+                .remove(&self.server_track.sid);
+        }
     }
 
-    pub fn disable(&self) -> impl Future<Output = Result<()>> {
-        async { Ok(()) }
+    pub fn stop(&self) {
+        if let Some(room) = self.room.upgrade() {
+            room.0
+                .lock()
+                .paused_audio_tracks
+                .insert(self.server_track.sid.clone());
+        }
+    }
+
+    pub fn is_playing(&self) -> bool {
+        !self
+            .room
+            .upgrade()
+            .unwrap()
+            .0
+            .lock()
+            .paused_audio_tracks
+            .contains(&self.server_track.sid)
     }
 }
 

crates/outline/src/outline.rs 🔗

@@ -64,7 +64,7 @@ impl ModalView for OutlineView {
 
 impl Render for OutlineView {
     fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
-        v_stack().w(rems(34.)).child(self.picker.clone())
+        v_flex().w(rems(34.)).child(self.picker.clone())
     }
 }
 

crates/picker/src/picker.rs 🔗

@@ -5,7 +5,7 @@ use gpui::{
     View, ViewContext, WindowContext,
 };
 use std::{cmp, sync::Arc};
-use ui::{prelude::*, v_stack, Color, Divider, Label, ListItem, ListItemSpacing, ListSeparator};
+use ui::{prelude::*, v_flex, Color, Divider, Label, ListItem, ListItemSpacing, ListSeparator};
 use workspace::ModalView;
 
 pub struct Picker<D: PickerDelegate> {
@@ -236,7 +236,7 @@ impl<D: PickerDelegate> ModalView for Picker<D> {}
 
 impl<D: PickerDelegate> Render for Picker<D> {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
-        let picker_editor = h_stack()
+        let picker_editor = h_flex()
             .overflow_hidden()
             .flex_none()
             .h_9()
@@ -264,7 +264,7 @@ impl<D: PickerDelegate> Render for Picker<D> {
             .child(Divider::horizontal())
             .when(self.delegate.match_count() > 0, |el| {
                 el.child(
-                    v_stack()
+                    v_flex()
                         .flex_grow()
                         .py_2()
                         .max_h(self.max_height.unwrap_or(rems(18.).into()))
@@ -309,7 +309,7 @@ impl<D: PickerDelegate> Render for Picker<D> {
             })
             .when(self.delegate.match_count() == 0, |el| {
                 el.child(
-                    v_stack().flex_grow().py_2().child(
+                    v_flex().flex_grow().py_2().child(
                         ListItem::new("empty_state")
                             .inset(true)
                             .spacing(ListItemSpacing::Sparse)

crates/project/src/project.rs 🔗

@@ -99,20 +99,6 @@ pub trait Item {
     fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
 }
 
-// Language server state is stored across 3 collections:
-//     language_servers =>
-//         a mapping from unique server id to LanguageServerState which can either be a task for a
-//         server in the process of starting, or a running server with adapter and language server arcs
-//     language_server_ids => a mapping from worktreeId and server name to the unique server id
-//     language_server_statuses => a mapping from unique server id to the current server status
-//
-// Multiple worktrees can map to the same language server for example when you jump to the definition
-// of a file in the standard library. So language_server_ids is used to look up which server is active
-// for a given worktree and language server name
-//
-// When starting a language server, first the id map is checked to make sure a server isn't already available
-// for that worktree. If there is one, it finishes early. Otherwise, a new id is allocated and and
-// the Starting variant of LanguageServerState is stored in the language_servers map.
 pub struct Project {
     worktrees: Vec<WorktreeHandle>,
     active_entry: Option<ProjectEntryId>,

crates/project_panel/src/project_panel.rs 🔗

@@ -1,6 +1,6 @@
 pub mod file_associations;
 mod project_panel_settings;
-use settings::{Settings, SettingsStore};
+use settings::Settings;
 
 use db::kvp::KEY_VALUE_STORE;
 use editor::{scroll::autoscroll::Autoscroll, Cancel, Editor};
@@ -30,7 +30,7 @@ use std::{
     sync::Arc,
 };
 use theme::ThemeSettings;
-use ui::{prelude::*, v_stack, ContextMenu, Icon, KeyBinding, Label, ListItem};
+use ui::{prelude::*, v_flex, ContextMenu, Icon, KeyBinding, Label, ListItem};
 use unicase::UniCase;
 use util::{maybe, ResultExt, TryFutureExt};
 use workspace::{
@@ -58,6 +58,7 @@ pub struct ProjectPanel {
     workspace: WeakView<Workspace>,
     width: Option<Pixels>,
     pending_serialization: Task<Option<()>>,
+    was_deserialized: bool,
 }
 
 #[derive(Copy, Clone, Debug)]
@@ -221,10 +222,10 @@ impl ProjectPanel {
             })
             .detach();
 
-            // cx.observe_global::<FileAssociations, _>(|_, cx| {
-            //     cx.notify();
-            // })
-            // .detach();
+            cx.observe_global::<FileAssociations>(|_, cx| {
+                cx.notify();
+            })
+            .detach();
 
             let mut this = Self {
                 project: project.clone(),
@@ -243,21 +244,10 @@ impl ProjectPanel {
                 workspace: workspace.weak_handle(),
                 width: None,
                 pending_serialization: Task::ready(None),
+                was_deserialized: false,
             };
             this.update_visible_entries(None, cx);
 
-            // Update the dock position when the setting changes.
-            let mut old_dock_position = this.position(cx);
-            ProjectPanelSettings::register(cx);
-            cx.observe_global::<SettingsStore>(move |this, cx| {
-                let new_dock_position = this.position(cx);
-                if new_dock_position != old_dock_position {
-                    old_dock_position = new_dock_position;
-                    cx.emit(PanelEvent::ChangePosition);
-                }
-            })
-            .detach();
-
             this
         });
 
@@ -292,16 +282,16 @@ impl ProjectPanel {
                 }
                 &Event::SplitEntry { entry_id } => {
                     if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
-                        if let Some(_entry) = worktree.read(cx).entry_for_id(entry_id) {
-                            // workspace
-                            //     .split_path(
-                            //         ProjectPath {
-                            //             worktree_id: worktree.read(cx).id(),
-                            //             path: entry.path.clone(),
-                            //         },
-                            //         cx,
-                            //     )
-                            //     .detach_and_log_err(cx);
+                        if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) {
+                            workspace
+                                .split_path(
+                                    ProjectPath {
+                                        worktree_id: worktree.read(cx).id(),
+                                        path: entry.path.clone(),
+                                    },
+                                    cx,
+                                )
+                                .detach_and_log_err(cx);
                         }
                     }
                 }
@@ -334,6 +324,7 @@ impl ProjectPanel {
             if let Some(serialized_panel) = serialized_panel {
                 panel.update(cx, |panel, cx| {
                     panel.width = serialized_panel.width;
+                    panel.was_deserialized = true;
                     cx.notify();
                 });
             }
@@ -788,10 +779,6 @@ impl ProjectPanel {
                     cx.notify();
                 }
             }
-
-            // cx.update_global(|drag_and_drop: &mut DragAndDrop<Workspace>, cx| {
-            //     drag_and_drop.cancel_dragging::<ProjectEntryId>(cx);
-            // })
         }
     }
 
@@ -1481,6 +1468,9 @@ impl ProjectPanel {
             cx.notify();
         }
     }
+    pub fn was_deserialized(&self) -> bool {
+        self.was_deserialized
+    }
 }
 
 impl Render for ProjectPanel {
@@ -1557,7 +1547,7 @@ impl Render for ProjectPanel {
                         .child(menu.clone())
                 }))
         } else {
-            v_stack()
+            v_flex()
                 .id("empty-project_panel")
                 .size_full()
                 .p_4()
@@ -1581,7 +1571,7 @@ impl Render for DraggedProjectEntryView {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Element {
         let settings = ProjectPanelSettings::get_global(cx);
         let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
-        h_stack()
+        h_flex()
             .font(ui_font)
             .bg(cx.theme().colors().background)
             .w(self.width)

crates/project_symbols/src/project_symbols.rs 🔗

@@ -11,7 +11,7 @@ use std::{borrow::Cow, cmp::Reverse, sync::Arc};
 use theme::ActiveTheme;
 use util::ResultExt;
 use workspace::{
-    ui::{v_stack, Color, Label, LabelCommon, LabelLike, ListItem, ListItemSpacing, Selectable},
+    ui::{v_flex, Color, Label, LabelCommon, LabelLike, ListItem, ListItemSpacing, Selectable},
     Workspace,
 };
 
@@ -242,7 +242,7 @@ impl PickerDelegate for ProjectSymbolsDelegate {
                 .spacing(ListItemSpacing::Sparse)
                 .selected(selected)
                 .child(
-                    v_stack()
+                    v_flex()
                         .child(
                             LabelLike::new().child(
                                 StyledText::new(label)

crates/recent_projects/src/recent_projects.rs 🔗

@@ -104,7 +104,7 @@ impl FocusableView for RecentProjects {
 
 impl Render for RecentProjects {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
-        v_stack()
+        v_flex()
             .w(rems(self.rem_width))
             .child(self.picker.clone())
             .on_mouse_down_out(cx.listener(|this, _, cx| {
@@ -236,7 +236,7 @@ impl PickerDelegate for RecentProjectsDelegate {
                 .spacing(ListItemSpacing::Sparse)
                 .selected(selected)
                 .child(
-                    v_stack()
+                    v_flex()
                         .child(highlighted_location.names)
                         .when(self.render_paths, |this| {
                             this.children(highlighted_location.paths)

crates/refineable/derive_refineable/src/derive_refineable.rs 🔗

@@ -69,13 +69,6 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream {
                         path: parse_quote!(Clone),
                     }));
 
-                    // punctuated.push_punct(syn::token::Add::default());
-                    // punctuated.push_value(TypeParamBound::Trait(TraitBound {
-                    //     paren_token: None,
-                    //     modifier: syn::TraitBoundModifier::None,
-                    //     lifetimes: None,
-                    //     path: parse_quote!(Default),
-                    // }));
                     punctuated
                 },
             })
@@ -94,10 +87,6 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream {
         },
     };
 
-    // refinable_refine_assignments
-    // refinable_refined_assignments
-    // refinement_refine_assignments
-
     let refineable_refine_assignments: Vec<TokenStream2> = fields
         .iter()
         .map(|field| {

crates/rich_text/src/rich_text.rs 🔗

@@ -39,6 +39,7 @@ pub struct RichText {
 
 /// Allows one to specify extra links to the rendered markdown, which can be used
 /// for e.g. mentions.
+#[derive(Debug)]
 pub struct Mention {
     pub range: Range<usize>,
     pub is_self_mention: bool,
@@ -85,31 +86,6 @@ impl RichText {
         })
         .into_any_element()
     }
-
-    // pub fn add_mention(
-    //     &mut self,
-    //     range: Range<usize>,
-    //     is_current_user: bool,
-    //     mention_style: HighlightStyle,
-    // ) -> anyhow::Result<()> {
-    //     if range.end > self.text.len() {
-    //         bail!(
-    //             "Mention in range {range:?} is outside of bounds for a message of length {}",
-    //             self.text.len()
-    //         );
-    //     }
-
-    //     if is_current_user {
-    //         self.region_ranges.push(range.clone());
-    //         self.regions.push(RenderedRegion {
-    //             background_kind: Some(BackgroundKind::Mention),
-    //             link_url: None,
-    //         });
-    //     }
-    //     self.highlights
-    //         .push((range, Highlight::Highlight(mention_style)));
-    //     Ok(())
-    // }
 }
 
 pub fn render_markdown_mut(
@@ -138,20 +114,21 @@ pub fn render_markdown_mut(
                 if let Some(language) = &current_language {
                     render_code(text, highlights, t.as_ref(), language);
                 } else {
-                    if let Some(mention) = mentions.first() {
-                        if source_range.contains_inclusive(&mention.range) {
-                            mentions = &mentions[1..];
-                            let range = (prev_len + mention.range.start - source_range.start)
-                                ..(prev_len + mention.range.end - source_range.start);
-                            highlights.push((
-                                range.clone(),
-                                if mention.is_self_mention {
-                                    Highlight::SelfMention
-                                } else {
-                                    Highlight::Mention
-                                },
-                            ));
+                    while let Some(mention) = mentions.first() {
+                        if !source_range.contains_inclusive(&mention.range) {
+                            break;
                         }
+                        mentions = &mentions[1..];
+                        let range = (prev_len + mention.range.start - source_range.start)
+                            ..(prev_len + mention.range.end - source_range.start);
+                        highlights.push((
+                            range.clone(),
+                            if mention.is_self_mention {
+                                Highlight::SelfMention
+                            } else {
+                                Highlight::Mention
+                            },
+                        ));
                     }
 
                     text.push_str(t.as_ref());
@@ -272,13 +249,6 @@ pub fn render_markdown(
     language_registry: &Arc<LanguageRegistry>,
     language: Option<&Arc<Language>>,
 ) -> RichText {
-    // let mut data = RichText {
-    //     text: Default::default(),
-    //     highlights: Default::default(),
-    //     region_ranges: Default::default(),
-    //     regions: Default::default(),
-    // };
-
     let mut text = String::new();
     let mut highlights = Vec::new();
     let mut link_ranges = Vec::new();

crates/rpc/build.rs 🔗

@@ -1,6 +1,5 @@
 fn main() {
     let mut build = prost_build::Config::new();
-    // build.protoc_arg("--experimental_allow_proto3_optional");
     build
         .type_attribute(".", "#[derive(serde::Serialize)]")
         .compile_protos(&["proto/zed.proto"], &["proto"])

crates/search/src/buffer_search.rs 🔗

@@ -21,7 +21,7 @@ use settings::Settings;
 use std::{any::Any, sync::Arc};
 use theme::ThemeSettings;
 
-use ui::{h_stack, prelude::*, Icon, IconButton, IconName, ToggleButton, Tooltip};
+use ui::{h_flex, prelude::*, Icon, IconButton, IconName, ToggleButton, Tooltip};
 use util::ResultExt;
 use workspace::{
     item::ItemHandle,
@@ -186,7 +186,7 @@ impl Render for BufferSearchBar {
         } else {
             cx.theme().colors().border
         };
-        h_stack()
+        h_flex()
             .w_full()
             .gap_2()
             .key_context(key_context)
@@ -216,7 +216,7 @@ impl Render for BufferSearchBar {
                 this.on_action(cx.listener(Self::toggle_whole_word))
             })
             .child(
-                h_stack()
+                h_flex()
                     .flex_1()
                     .px_2()
                     .py_1()
@@ -243,11 +243,11 @@ impl Render for BufferSearchBar {
                     })),
             )
             .child(
-                h_stack()
+                h_flex()
                     .gap_2()
                     .flex_none()
                     .child(
-                        h_stack()
+                        h_flex()
                             .child(
                                 ToggleButton::new("search-mode-text", SearchMode::Text.label())
                                     .style(ButtonStyle::Filled)
@@ -303,12 +303,12 @@ impl Render for BufferSearchBar {
                     }),
             )
             .child(
-                h_stack()
+                h_flex()
                     .gap_0p5()
                     .flex_1()
                     .when(self.replace_enabled, |this| {
                         this.child(
-                            h_stack()
+                            h_flex()
                                 .flex_1()
                                 // We're giving this a fixed height to match the height of the search input,
                                 // which has an icon inside that is increasing its height.
@@ -346,7 +346,7 @@ impl Render for BufferSearchBar {
                     }),
             )
             .child(
-                h_stack()
+                h_flex()
                     .gap_0p5()
                     .flex_none()
                     .child(
@@ -1648,7 +1648,6 @@ mod tests {
 
     #[gpui::test]
     async fn test_search_query_history(cx: &mut TestAppContext) {
-        //crate::project_search::tests::init_test(cx);
         init_globals(cx);
         let buffer_text = r#"
         A regular expression (shortened as regex or regexp;[1] also referred to as

crates/search/src/project_search.rs 🔗

@@ -38,7 +38,7 @@ use std::{
 use theme::ThemeSettings;
 
 use ui::{
-    h_stack, prelude::*, v_stack, Icon, IconButton, IconName, Label, LabelCommon, LabelSize,
+    h_flex, prelude::*, v_flex, Icon, IconButton, IconName, Label, LabelCommon, LabelSize,
     Selectable, ToggleButton, Tooltip,
 };
 use util::{paths::PathMatcher, ResultExt as _};
@@ -360,19 +360,19 @@ impl Render for ProjectSearchView {
                     .max_w_96()
                     .child(Label::new(text).size(LabelSize::Small))
             });
-            v_stack()
+            v_flex()
                 .flex_1()
                 .size_full()
                 .justify_center()
                 .bg(cx.theme().colors().editor_background)
                 .track_focus(&self.focus_handle)
                 .child(
-                    h_stack()
+                    h_flex()
                         .size_full()
                         .justify_center()
-                        .child(h_stack().flex_1())
-                        .child(v_stack().child(major_text).children(minor_text))
-                        .child(h_stack().flex_1()),
+                        .child(h_flex().flex_1())
+                        .child(v_flex().child(major_text).children(minor_text))
+                        .child(h_flex().flex_1()),
                 )
         }
     }
@@ -431,7 +431,7 @@ impl Item for ProjectSearchView {
         let tab_name = last_query
             .filter(|query| !query.is_empty())
             .unwrap_or_else(|| "Project search".into());
-        h_stack()
+        h_flex()
             .gap_2()
             .child(Icon::new(IconName::MagnifyingGlass).color(if selected {
                 Color::Default
@@ -446,6 +446,10 @@ impl Item for ProjectSearchView {
             .into_any()
     }
 
+    fn telemetry_event_text(&self) -> Option<&'static str> {
+        Some("project search")
+    }
+
     fn for_each_project_item(
         &self,
         cx: &AppContext,
@@ -1601,8 +1605,8 @@ impl Render for ProjectSearchBar {
         let search = search.read(cx);
         let semantic_is_available = SemanticIndex::enabled(cx);
 
-        let query_column = v_stack().child(
-            h_stack()
+        let query_column = v_flex().child(
+            h_flex()
                 .min_w(rems(512. / 16.))
                 .px_2()
                 .py_1()
@@ -1617,7 +1621,7 @@ impl Render for ProjectSearchBar {
                 .child(Icon::new(IconName::MagnifyingGlass))
                 .child(self.render_text_input(&search.query_editor, cx))
                 .child(
-                    h_stack()
+                    h_flex()
                         .child(
                             IconButton::new("project-search-filter-button", IconName::Filter)
                                 .tooltip(|cx| {
@@ -1674,11 +1678,11 @@ impl Render for ProjectSearchBar {
                 ),
         );
 
-        let mode_column = v_stack().items_start().justify_start().child(
-            h_stack()
+        let mode_column = v_flex().items_start().justify_start().child(
+            h_flex()
                 .gap_2()
                 .child(
-                    h_stack()
+                    h_flex()
                         .child(
                             ToggleButton::new("project-search-text-button", "Text")
                                 .style(ButtonStyle::Filled)
@@ -1744,7 +1748,7 @@ impl Render for ProjectSearchBar {
                 ),
         );
         let replace_column = if search.replace_enabled {
-            h_stack()
+            h_flex()
                 .flex_1()
                 .h_full()
                 .gap_2()
@@ -1757,9 +1761,9 @@ impl Render for ProjectSearchBar {
                 .child(self.render_text_input(&search.replacement_editor, cx))
         } else {
             // Fill out the space if we don't have a replacement editor.
-            h_stack().flex_1()
+            h_flex().flex_1()
         };
-        let actions_column = h_stack()
+        let actions_column = h_flex()
             .when(search.replace_enabled, |this| {
                 this.child(
                     IconButton::new("project-search-replace-next", IconName::ReplaceNext)
@@ -1820,7 +1824,7 @@ impl Render for ProjectSearchBar {
                     .tooltip(|cx| Tooltip::for_action("Go to next match", &SelectNextMatch, cx)),
             );
 
-        v_stack()
+        v_flex()
             .key_context(key_context)
             .flex_grow()
             .gap_2()
@@ -1880,7 +1884,7 @@ impl Render for ProjectSearchBar {
                 })
             })
             .child(
-                h_stack()
+                h_flex()
                     .justify_between()
                     .gap_2()
                     .child(query_column)
@@ -1890,12 +1894,12 @@ impl Render for ProjectSearchBar {
             )
             .when(search.filters_enabled, |this| {
                 this.child(
-                    h_stack()
+                    h_flex()
                         .flex_1()
                         .gap_2()
                         .justify_between()
                         .child(
-                            h_stack()
+                            h_flex()
                                 .flex_1()
                                 .h_full()
                                 .px_2()
@@ -1921,7 +1925,7 @@ impl Render for ProjectSearchBar {
                                 }),
                         )
                         .child(
-                            h_stack()
+                            h_flex()
                                 .flex_1()
                                 .h_full()
                                 .px_2()

crates/semantic_index/src/semantic_index_tests.rs 🔗

@@ -1677,8 +1677,6 @@ fn elixir_lang() -> Arc<Language> {
 
 #[gpui::test]
 fn test_subtract_ranges() {
-    // collapsed_ranges: Vec<Range<usize>>, keep_ranges: Vec<Range<usize>>
-
     assert_eq!(
         subtract_ranges(&[0..5, 10..21], &[0..1, 4..5]),
         vec![1..4, 10..21]

crates/story/src/story.rs 🔗

@@ -255,8 +255,8 @@ impl Story {
             .child(label.into())
     }
 
-    /// Note: Not ui::v_stack() as the story crate doesn't depend on the ui crate.
-    pub fn v_stack() -> Div {
+    /// Note: Not `ui::v_flex` as the `story` crate doesn't depend on the `ui` crate.
+    pub fn v_flex() -> Div {
         div().flex().flex_col().gap_1()
     }
 }
@@ -298,7 +298,7 @@ impl RenderOnce for StoryItem {
             .gap_4()
             .w_full()
             .child(
-                Story::v_stack()
+                Story::v_flex()
                     .px_2()
                     .w_1_2()
                     .min_h_px()
@@ -319,7 +319,7 @@ impl RenderOnce for StoryItem {
                     }),
             )
             .child(
-                Story::v_stack()
+                Story::v_flex()
                     .px_2()
                     .flex_none()
                     .w_1_2()

crates/storybook/Cargo.toml 🔗

@@ -35,6 +35,7 @@ menu = { path = "../menu" }
 ui = { path = "../ui", features = ["stories"] }
 util = { path = "../util" }
 picker = { path = "../picker" }
+ctrlc = "3.4"
 
 [dev-dependencies]
 gpui = { path = "../gpui", features = ["test-support"] }

crates/storybook/src/stories/overflow_scroll.rs 🔗

@@ -11,7 +11,7 @@ impl Render for OverflowScrollStory {
             .child(Story::title("Overflow Scroll"))
             .child(Story::label("`overflow_x_scroll`"))
             .child(
-                h_stack()
+                h_flex()
                     .id("overflow_x_scroll")
                     .gap_2()
                     .overflow_x_scroll()
@@ -24,7 +24,7 @@ impl Render for OverflowScrollStory {
             )
             .child(Story::label("`overflow_y_scroll`"))
             .child(
-                v_stack()
+                v_flex()
                     .id("overflow_y_scroll")
                     .gap_2()
                     .overflow_y_scroll()

crates/storybook/src/stories/text.rs 🔗

@@ -117,7 +117,7 @@ impl Render for TextStory {
 //     type Element = Div;
 
 //     fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
-//         v_stack()
+//         v_flex()
 //             .bg(blue())
 //             .child(
 //                 div()

crates/storybook/src/storybook.rs 🔗

@@ -21,11 +21,6 @@ use crate::assets::Assets;
 use crate::story_selector::{ComponentStory, StorySelector};
 pub use indoc::indoc;
 
-// gpui::actions! {
-//     storybook,
-//     [ToggleInspector]
-// }
-
 #[derive(Parser)]
 #[command(author, version, about, long_about = None)]
 struct Args {
@@ -49,11 +44,17 @@ fn main() {
     let story_selector = args.story.clone().unwrap_or_else(|| {
         let stories = ComponentStory::iter().collect::<Vec<_>>();
 
-        let selection = FuzzySelect::new()
+        ctrlc::set_handler(move || {}).unwrap();
+
+        let result = FuzzySelect::new()
             .with_prompt("Choose a story to run:")
             .items(&stories)
-            .interact()
-            .unwrap();
+            .interact();
+
+        let Ok(selection) = result else {
+            dialoguer::console::Term::stderr().show_cursor().unwrap();
+            std::process::exit(0);
+        };
 
         StorySelector::Component(stories[selection])
     });

crates/terminal_view/src/terminal_panel.rs 🔗

@@ -11,9 +11,9 @@ use itertools::Itertools;
 use project::{Fs, ProjectEntryId};
 use search::{buffer_search::DivRegistrar, BufferSearchBar};
 use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsStore};
+use settings::Settings;
 use terminal::terminal_settings::{TerminalDockPosition, TerminalSettings};
-use ui::{h_stack, ButtonCommon, Clickable, IconButton, IconSize, Selectable, Tooltip};
+use ui::{h_flex, ButtonCommon, Clickable, IconButton, IconSize, Selectable, Tooltip};
 use util::{ResultExt, TryFutureExt};
 use workspace::{
     dock::{DockPosition, Panel, PanelEvent},
@@ -68,7 +68,7 @@ impl TerminalPanel {
             pane.display_nav_history_buttons(false);
             pane.set_render_tab_bar_buttons(cx, move |pane, cx| {
                 let terminal_panel = terminal_panel.clone();
-                h_stack()
+                h_flex()
                     .gap_2()
                     .child(
                         IconButton::new("plus", IconName::Plus)
@@ -159,15 +159,6 @@ impl TerminalPanel {
             height: None,
             _subscriptions: subscriptions,
         };
-        let mut old_dock_position = this.position(cx);
-        cx.observe_global::<SettingsStore>(move |this, cx| {
-            let new_dock_position = this.position(cx);
-            if new_dock_position != old_dock_position {
-                old_dock_position = new_dock_position;
-                cx.emit(PanelEvent::ChangePosition);
-            }
-        })
-        .detach();
         this
     }
 

crates/terminal_view/src/terminal_view.rs 🔗

@@ -20,7 +20,7 @@ use terminal::{
     Clear, Copy, Event, MaybeNavigationTarget, Paste, ShowCharacterPalette, Terminal,
 };
 use terminal_element::TerminalElement;
-use ui::{h_stack, prelude::*, ContextMenu, Icon, IconName, Label};
+use ui::{h_flex, prelude::*, ContextMenu, Icon, IconName, Label};
 use util::{paths::PathLikeWithPosition, ResultExt};
 use workspace::{
     item::{BreadcrumbText, Item, ItemEvent},
@@ -600,6 +600,9 @@ fn possible_open_targets(
 
 pub fn regex_search_for_query(query: &project::search::SearchQuery) -> Option<RegexSearch> {
     let query = query.as_str();
+    if query == "." {
+        return None;
+    }
     let searcher = RegexSearch::new(&query);
     searcher.ok()
 }
@@ -694,7 +697,7 @@ impl Item for TerminalView {
         cx: &WindowContext,
     ) -> AnyElement {
         let title = self.terminal().read(cx).title(true);
-        h_stack()
+        h_flex()
             .gap_2()
             .child(Icon::new(IconName::Terminal))
             .child(Label::new(title).color(if selected {
@@ -705,6 +708,10 @@ impl Item for TerminalView {
             .into_any()
     }
 
+    fn telemetry_event_text(&self) -> Option<&'static str> {
+        None
+    }
+
     fn clone_on_split(
         &self,
         _workspace_id: WorkspaceId,

crates/theme/src/default_colors.rs 🔗

@@ -8,6 +8,11 @@ pub(crate) fn neutral() -> ColorScaleSet {
     sand()
 }
 
+// Note: We aren't currently making use of the default colors, as all of the
+// themes have a value set for each color.
+//
+// We'll need to revisit these once we're ready to launch user themes, which may
+// not specify a value for each color (and thus should fall back to the defaults).
 impl ThemeColors {
     pub fn light() -> Self {
         let system = SystemColors::default();
@@ -23,12 +28,12 @@ impl ThemeColors {
             surface_background: neutral().light().step_2(),
             background: neutral().light().step_1(),
             element_background: neutral().light().step_3(),
-            element_hover: neutral().light_alpha().step_4(), // todo!("pick the right colors")
+            element_hover: neutral().light_alpha().step_4(),
             element_active: neutral().light_alpha().step_5(),
             element_selected: neutral().light_alpha().step_5(),
-            element_disabled: neutral().light_alpha().step_3(), // todo!("pick the right colors")
-            drop_target_background: blue().light_alpha().step_2(), // todo!("pick the right colors")
-            ghost_element_background: system.transparent,       // todo!("pick the right colors")
+            element_disabled: neutral().light_alpha().step_3(),
+            drop_target_background: blue().light_alpha().step_2(),
+            ghost_element_background: system.transparent,
             ghost_element_hover: neutral().light_alpha().step_3(),
             ghost_element_active: neutral().light_alpha().step_4(),
             ghost_element_selected: neutral().light_alpha().step_5(),
@@ -59,7 +64,7 @@ impl ThemeColors {
             scrollbar_track_background: gpui::transparent_black(),
             scrollbar_track_border: neutral().light().step_5(),
             editor_foreground: neutral().light().step_12(),
-            editor_background: neutral().light().step_1(), // todo!(this was inserted by Mikayla)
+            editor_background: neutral().light().step_1(),
             editor_gutter_background: neutral().light().step_1(),
             editor_subheader_background: neutral().light().step_2(),
             editor_active_line_background: neutral().light_alpha().step_3(),
@@ -106,17 +111,17 @@ impl ThemeColors {
             surface_background: neutral().dark().step_2(),
             background: neutral().dark().step_1(),
             element_background: neutral().dark().step_3(),
-            element_hover: neutral().dark_alpha().step_4(), // todo!("pick the right colors")
+            element_hover: neutral().dark_alpha().step_4(),
             element_active: neutral().dark_alpha().step_5(),
-            element_selected: neutral().dark_alpha().step_5(), // todo!("pick the right colors")
-            element_disabled: neutral().dark_alpha().step_3(), // todo!("pick the right colors")
+            element_selected: neutral().dark_alpha().step_5(),
+            element_disabled: neutral().dark_alpha().step_3(),
             drop_target_background: blue().dark_alpha().step_2(),
             ghost_element_background: system.transparent,
-            ghost_element_hover: neutral().dark_alpha().step_4(), // todo!("pick the right colors")
-            ghost_element_active: neutral().dark_alpha().step_5(), // todo!("pick the right colors")
+            ghost_element_hover: neutral().dark_alpha().step_4(),
+            ghost_element_active: neutral().dark_alpha().step_5(),
             ghost_element_selected: neutral().dark_alpha().step_5(),
             ghost_element_disabled: neutral().dark_alpha().step_3(),
-            text: neutral().dark().step_12(), // todo!("pick the right colors")
+            text: neutral().dark().step_12(),
             text_muted: neutral().dark().step_11(),
             text_placeholder: neutral().dark().step_10(),
             text_disabled: neutral().dark().step_9(),
@@ -140,7 +145,7 @@ impl ThemeColors {
             scrollbar_thumb_hover_background: neutral().dark_alpha().step_4(),
             scrollbar_thumb_border: gpui::transparent_black(),
             scrollbar_track_background: gpui::transparent_black(),
-            scrollbar_track_border: neutral().dark().step_5(), // todo!(this was inserted by Mikayla)
+            scrollbar_track_border: neutral().dark().step_5(),
             editor_foreground: neutral().dark().step_12(),
             editor_background: neutral().dark().step_1(),
             editor_gutter_background: neutral().dark().step_1(),

crates/theme/src/one_themes.rs 🔗

@@ -7,6 +7,10 @@ use crate::{
     ThemeColors, ThemeFamily, ThemeStyles,
 };
 
+// Note: This theme family is not the one you see in Zed at the moment.
+// This is a from-scratch rebuild that Nate started work on. We currently
+// only use this in the tests, and the One family from the `themes/` directory
+// is what gets loaded into Zed when running it.
 pub fn one_family() -> ThemeFamily {
     ThemeFamily {
         id: "one".to_string(),
@@ -75,7 +79,7 @@ pub(crate) fn one_dark() -> Theme {
                 tab_bar_background: bg,
                 tab_inactive_background: bg,
                 tab_active_background: editor,
-                search_match_background: bg, // todo!(this was inserted by Mikayla)
+                search_match_background: bg,
 
                 editor_background: editor,
                 editor_gutter_background: editor,

crates/theme/src/settings.rs 🔗

@@ -1,7 +1,9 @@
 use crate::one_themes::one_dark;
 use crate::{Theme, ThemeRegistry};
 use anyhow::Result;
-use gpui::{px, AppContext, Font, FontFeatures, FontStyle, FontWeight, Pixels};
+use gpui::{
+    px, AppContext, Font, FontFeatures, FontStyle, FontWeight, Pixels, Subscription, ViewContext,
+};
 use schemars::{
     gen::SchemaGenerator,
     schema::{InstanceType, Schema, SchemaObject},
@@ -80,6 +82,13 @@ impl ThemeSettings {
     }
 }
 
+pub fn observe_buffer_font_size_adjustment<V: 'static>(
+    cx: &mut ViewContext<V>,
+    f: impl 'static + Fn(&mut V, &mut ViewContext<V>),
+) -> Subscription {
+    cx.observe_global::<AdjustedBufferFontSize>(f)
+}
+
 pub fn adjusted_font_size(size: Pixels, cx: &mut AppContext) -> Pixels {
     if let Some(AdjustedBufferFontSize(adjusted_size)) = cx.try_global::<AdjustedBufferFontSize>() {
         let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size;
@@ -194,9 +203,21 @@ impl settings::Settings for ThemeSettings {
             ..Default::default()
         };
 
-        root_schema
-            .definitions
-            .extend([("ThemeName".into(), theme_name_schema.into())]);
+        let available_fonts = cx
+            .text_system()
+            .all_font_families()
+            .into_iter()
+            .map(Value::String)
+            .collect();
+        let fonts_schema = SchemaObject {
+            instance_type: Some(InstanceType::String.into()),
+            enum_values: Some(available_fonts),
+            ..Default::default()
+        };
+        root_schema.definitions.extend([
+            ("ThemeName".into(), theme_name_schema.into()),
+            ("FontFamilies".into(), fonts_schema.into()),
+        ]);
 
         root_schema
             .schema
@@ -204,10 +225,16 @@ impl settings::Settings for ThemeSettings {
             .as_mut()
             .unwrap()
             .properties
-            .extend([(
-                "theme".to_owned(),
-                Schema::new_ref("#/definitions/ThemeName".into()),
-            )]);
+            .extend([
+                (
+                    "theme".to_owned(),
+                    Schema::new_ref("#/definitions/ThemeName".into()),
+                ),
+                (
+                    "buffer_font_family".to_owned(),
+                    Schema::new_ref("#/definitions/FontFamilies".into()),
+                ),
+            ]);
 
         root_schema
     }

crates/theme/src/theme.rs 🔗

@@ -20,7 +20,7 @@ mod user_theme;
 
 use std::sync::Arc;
 
-use ::settings::Settings;
+use ::settings::{Settings, SettingsStore};
 pub use default_colors::*;
 pub use default_theme::*;
 pub use registry::*;
@@ -62,13 +62,21 @@ pub enum LoadThemes {
 
 pub fn init(themes_to_load: LoadThemes, cx: &mut AppContext) {
     cx.set_global(ThemeRegistry::default());
-
     match themes_to_load {
         LoadThemes::JustBase => (),
         LoadThemes::All => cx.global_mut::<ThemeRegistry>().load_user_themes(),
     }
-
     ThemeSettings::register(cx);
+
+    let mut prev_buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size;
+    cx.observe_global::<SettingsStore>(move |cx| {
+        let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size;
+        if buffer_font_size != prev_buffer_font_size {
+            prev_buffer_font_size = buffer_font_size;
+            reset_font_size(cx);
+        }
+    })
+    .detach();
 }
 
 pub trait ActiveTheme {

crates/theme_selector/src/theme_selector.rs 🔗

@@ -10,7 +10,7 @@ use picker::{Picker, PickerDelegate};
 use settings::{update_settings_file, SettingsStore};
 use std::sync::Arc;
 use theme::{Theme, ThemeMeta, ThemeRegistry, ThemeSettings};
-use ui::{prelude::*, v_stack, ListItem, ListItemSpacing};
+use ui::{prelude::*, v_flex, ListItem, ListItemSpacing};
 use util::ResultExt;
 use workspace::{ui::HighlightedLabel, ModalView, Workspace};
 
@@ -70,7 +70,7 @@ impl FocusableView for ThemeSelector {
 
 impl Render for ThemeSelector {
     fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
-        v_stack().w(rems(34.)).child(self.picker.clone())
+        v_flex().w(rems(34.)).child(self.picker.clone())
     }
 }
 

crates/ui/src/components/avatar.rs 🔗

@@ -26,6 +26,7 @@ pub enum AvatarShape {
 #[derive(IntoElement)]
 pub struct Avatar {
     image: Img,
+    size: Option<Pixels>,
     border_color: Option<Hsla>,
     is_available: Option<bool>,
 }
@@ -36,7 +37,7 @@ impl RenderOnce for Avatar {
             self = self.shape(AvatarShape::Circle);
         }
 
-        let size = cx.rem_size();
+        let size = self.size.unwrap_or_else(|| cx.rem_size());
 
         div()
             .size(size + px(2.))
@@ -78,6 +79,7 @@ impl Avatar {
             image: img(src),
             is_available: None,
             border_color: None,
+            size: None,
         }
     }
 
@@ -124,4 +126,10 @@ impl Avatar {
         self.is_available = is_available.into();
         self
     }
+
+    /// Size overrides the avatar size. By default they are 1rem.
+    pub fn size(mut self, size: impl Into<Option<Pixels>>) -> Self {
+        self.size = size.into();
+        self
+    }
 }

crates/ui/src/components/button/button.rs 🔗

@@ -362,7 +362,7 @@ impl RenderOnce for Button {
         };
 
         self.base.child(
-            h_stack()
+            h_flex()
                 .gap_1()
                 .when(self.icon_position == Some(IconPosition::Start), |this| {
                     this.children(self.icon.map(|icon| {
@@ -375,7 +375,7 @@ impl RenderOnce for Button {
                     }))
                 })
                 .child(
-                    h_stack()
+                    h_flex()
                         .gap_2()
                         .justify_between()
                         .child(

crates/ui/src/components/button/button_like.rs 🔗

@@ -300,6 +300,7 @@ pub struct ButtonLike {
     pub(super) selected: bool,
     pub(super) selected_style: Option<ButtonStyle>,
     pub(super) width: Option<DefiniteLength>,
+    pub(super) height: Option<DefiniteLength>,
     size: ButtonSize,
     rounding: Option<ButtonLikeRounding>,
     tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>,
@@ -317,6 +318,7 @@ impl ButtonLike {
             selected: false,
             selected_style: None,
             width: None,
+            height: None,
             size: ButtonSize::Default,
             rounding: Some(ButtonLikeRounding::All),
             tooltip: None,
@@ -325,6 +327,11 @@ impl ButtonLike {
         }
     }
 
+    pub(crate) fn height(mut self, height: DefiniteLength) -> Self {
+        self.height = Some(height);
+        self
+    }
+
     pub(crate) fn rounding(mut self, rounding: impl Into<Option<ButtonLikeRounding>>) -> Self {
         self.rounding = rounding.into();
         self
@@ -417,7 +424,7 @@ impl RenderOnce for ButtonLike {
             .id(self.id.clone())
             .group("")
             .flex_none()
-            .h(self.size.height())
+            .h(self.height.unwrap_or(self.size.height().into()))
             .when_some(self.width, |this, width| this.w(width).justify_center())
             .when_some(self.rounding, |this, rounding| match rounding {
                 ButtonLikeRounding::All => this.rounded_md(),

crates/ui/src/components/button/icon_button.rs 🔗

@@ -5,9 +5,17 @@ use crate::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, IconName, IconSiz
 
 use super::button_icon::ButtonIcon;
 
+/// The shape of an [`IconButton`].
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
+pub enum IconButtonShape {
+    Square,
+    Wide,
+}
+
 #[derive(IntoElement)]
 pub struct IconButton {
     base: ButtonLike,
+    shape: IconButtonShape,
     icon: IconName,
     icon_size: IconSize,
     icon_color: Color,
@@ -18,6 +26,7 @@ impl IconButton {
     pub fn new(id: impl Into<ElementId>, icon: IconName) -> Self {
         Self {
             base: ButtonLike::new(id),
+            shape: IconButtonShape::Wide,
             icon,
             icon_size: IconSize::default(),
             icon_color: Color::Default,
@@ -25,6 +34,11 @@ impl IconButton {
         }
     }
 
+    pub fn shape(mut self, shape: IconButtonShape) -> Self {
+        self.shape = shape;
+        self
+    }
+
     pub fn icon_size(mut self, icon_size: IconSize) -> Self {
         self.icon_size = icon_size;
         self
@@ -118,14 +132,21 @@ impl RenderOnce for IconButton {
         let is_selected = self.base.selected;
         let selected_style = self.base.selected_style;
 
-        self.base.child(
-            ButtonIcon::new(self.icon)
-                .disabled(is_disabled)
-                .selected(is_selected)
-                .selected_icon(self.selected_icon)
-                .when_some(selected_style, |this, style| this.selected_style(style))
-                .size(self.icon_size)
-                .color(self.icon_color),
-        )
+        self.base
+            .map(|this| match self.shape {
+                IconButtonShape::Square => this
+                    .width(self.icon_size.rems().into())
+                    .height(self.icon_size.rems().into()),
+                IconButtonShape::Wide => this,
+            })
+            .child(
+                ButtonIcon::new(self.icon)
+                    .disabled(is_disabled)
+                    .selected(is_selected)
+                    .selected_icon(self.selected_icon)
+                    .when_some(selected_style, |this, style| this.selected_style(style))
+                    .size(self.icon_size)
+                    .color(self.icon_color),
+            )
     }
 }

crates/ui/src/components/checkbox.rs 🔗

@@ -103,7 +103,7 @@ impl RenderOnce for Checkbox {
             ),
         };
 
-        h_stack()
+        h_flex()
             .id(self.id)
             // Rather than adding `px_1()` to add some space around the checkbox,
             // we use a larger parent element to create a slightly larger

crates/ui/src/components/context_menu.rs 🔗

@@ -1,5 +1,5 @@
 use crate::{
-    h_stack, prelude::*, v_stack, Icon, IconName, KeyBinding, Label, List, ListItem, ListSeparator,
+    h_flex, prelude::*, v_flex, Icon, IconName, KeyBinding, Label, List, ListItem, ListSeparator,
     ListSubHeader,
 };
 use gpui::{
@@ -234,7 +234,7 @@ impl ContextMenuItem {
 impl Render for ContextMenu {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
         div().elevation_2(cx).flex().flex_row().child(
-            v_stack()
+            v_flex()
                 .min_w(px(200.))
                 .track_focus(&self.focus_handle)
                 .on_mouse_down_out(cx.listener(|this, _, cx| this.cancel(&menu::Cancel, cx)))
@@ -277,7 +277,7 @@ impl Render for ContextMenu {
                             let menu = cx.view().downgrade();
 
                             let label_element = if let Some(icon) = icon {
-                                h_stack()
+                                h_flex()
                                     .gap_1()
                                     .child(Label::new(label.clone()))
                                     .child(Icon::new(*icon))
@@ -298,7 +298,7 @@ impl Render for ContextMenu {
                                     .ok();
                                 })
                                 .child(
-                                    h_stack()
+                                    h_flex()
                                         .w_full()
                                         .justify_between()
                                         .child(label_element)

crates/ui/src/components/keybinding.rs 🔗

@@ -1,4 +1,4 @@
-use crate::{h_stack, prelude::*, Icon, IconName, IconSize};
+use crate::{h_flex, prelude::*, Icon, IconName, IconSize};
 use gpui::{relative, rems, Action, FocusHandle, IntoElement, Keystroke};
 
 #[derive(IntoElement, Clone)]
@@ -12,13 +12,13 @@ pub struct KeyBinding {
 
 impl RenderOnce for KeyBinding {
     fn render(self, cx: &mut WindowContext) -> impl IntoElement {
-        h_stack()
+        h_flex()
             .flex_none()
             .gap_2()
             .children(self.key_binding.keystrokes().iter().map(|keystroke| {
                 let key_icon = Self::icon_for_key(&keystroke);
 
-                h_stack()
+                h_flex()
                     .flex_none()
                     .gap_0p5()
                     .p_0p5()

crates/ui/src/components/list/list.rs 🔗

@@ -1,7 +1,7 @@
 use gpui::AnyElement;
 use smallvec::SmallVec;
 
-use crate::{prelude::*, v_stack, Label, ListHeader};
+use crate::{prelude::*, v_flex, Label, ListHeader};
 
 #[derive(IntoElement)]
 pub struct List {
@@ -47,7 +47,7 @@ impl ParentElement for List {
 
 impl RenderOnce for List {
     fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
-        v_stack().w_full().py_1().children(self.header).map(|this| {
+        v_flex().w_full().py_1().children(self.header).map(|this| {
             match (self.children.is_empty(), self.toggle) {
                 (false, _) => this.children(self.children),
                 (true, Some(false)) => this,

crates/ui/src/components/list/list_header.rs 🔗

@@ -1,4 +1,4 @@
-use crate::{h_stack, prelude::*, Disclosure, Label};
+use crate::{h_flex, prelude::*, Disclosure, Label};
 use gpui::{AnyElement, ClickEvent};
 
 #[derive(IntoElement)]
@@ -76,7 +76,7 @@ impl Selectable for ListHeader {
 
 impl RenderOnce for ListHeader {
     fn render(self, cx: &mut WindowContext) -> impl IntoElement {
-        h_stack()
+        h_flex()
             .id(self.label.clone())
             .w_full()
             .relative()
@@ -95,7 +95,7 @@ impl RenderOnce for ListHeader {
                     .w_full()
                     .gap_1()
                     .child(
-                        h_stack()
+                        h_flex()
                             .gap_1()
                             .children(self.toggle.map(|is_open| {
                                 Disclosure::new("toggle", is_open).on_toggle(self.on_toggle)
@@ -109,7 +109,7 @@ impl RenderOnce for ListHeader {
                                     .child(Label::new(self.label.clone()).color(Color::Muted)),
                             ),
                     )
-                    .child(h_stack().children(self.end_slot))
+                    .child(h_flex().children(self.end_slot))
                     .when_some(self.end_hover_slot, |this, end_hover_slot| {
                         this.child(
                             div()

crates/ui/src/components/list/list_item.rs 🔗

@@ -146,7 +146,7 @@ impl ParentElement for ListItem {
 
 impl RenderOnce for ListItem {
     fn render(self, cx: &mut WindowContext) -> impl IntoElement {
-        h_stack()
+        h_flex()
             .id(self.id)
             .w_full()
             .relative()
@@ -169,7 +169,7 @@ impl RenderOnce for ListItem {
                     })
             })
             .child(
-                h_stack()
+                h_flex()
                     .id("inner_list_item")
                     .w_full()
                     .relative()
@@ -219,9 +219,9 @@ impl RenderOnce for ListItem {
                             .child(Disclosure::new("toggle", is_open).on_toggle(self.on_toggle))
                     }))
                     .child(
-                        h_stack()
+                        h_flex()
                             // HACK: We need to set *any* width value here in order for this container to size correctly.
-                            // Without this the `h_stack` will overflow the parent `inner_list_item`.
+                            // Without this the `h_flex` will overflow the parent `inner_list_item`.
                             .w_px()
                             .flex_1()
                             .gap_1()
@@ -230,7 +230,7 @@ impl RenderOnce for ListItem {
                     )
                     .when_some(self.end_slot, |this, end_slot| {
                         this.justify_between().child(
-                            h_stack()
+                            h_flex()
                                 .when(self.end_hover_slot.is_some(), |this| {
                                     this.visible()
                                         .group_hover("list_item", |this| this.invisible())
@@ -240,7 +240,7 @@ impl RenderOnce for ListItem {
                     })
                     .when_some(self.end_hover_slot, |this, end_hover_slot| {
                         this.child(
-                            h_stack()
+                            h_flex()
                                 .h_full()
                                 .absolute()
                                 .right_2()

crates/ui/src/components/list/list_sub_header.rs 🔗

@@ -1,5 +1,5 @@
 use crate::prelude::*;
-use crate::{h_stack, Icon, IconName, IconSize, Label};
+use crate::{h_flex, Icon, IconName, IconSize, Label};
 
 #[derive(IntoElement)]
 pub struct ListSubHeader {
@@ -25,7 +25,7 @@ impl ListSubHeader {
 
 impl RenderOnce for ListSubHeader {
     fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
-        h_stack().flex_1().w_full().relative().py_1().child(
+        h_flex().flex_1().w_full().relative().py_1().child(
             div()
                 .h_6()
                 .when(self.inset, |this| this.px_2())

crates/ui/src/components/popover.rs 🔗

@@ -1,5 +1,5 @@
 use crate::prelude::*;
-use crate::v_stack;
+use crate::v_flex;
 use gpui::{
     div, AnyElement, Element, IntoElement, ParentElement, RenderOnce, Styled, WindowContext,
 };
@@ -43,10 +43,10 @@ impl RenderOnce for Popover {
         div()
             .flex()
             .gap_1()
-            .child(v_stack().elevation_2(cx).px_1().children(self.children))
+            .child(v_flex().elevation_2(cx).px_1().children(self.children))
             .when_some(self.aside, |this, aside| {
                 this.child(
-                    v_stack()
+                    v_flex()
                         .elevation_2(cx)
                         .bg(cx.theme().colors().surface_background)
                         .px_1()

crates/ui/src/components/right_click_menu.rs 🔗

@@ -134,6 +134,7 @@ impl<M: ManagedView> Element for RightClickMenu<M> {
         let position = element_state.position.clone();
         let attach = self.attach.clone();
         let child_layout_id = element_state.child_layout_id.clone();
+        let child_bounds = cx.layout_bounds(child_layout_id.unwrap());
 
         cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
             if phase == DispatchPhase::Bubble
@@ -154,20 +155,18 @@ impl<M: ManagedView> Element for RightClickMenu<M> {
                         }
                     }
                     *menu2.borrow_mut() = None;
-                    cx.notify();
+                    cx.refresh();
                 })
                 .detach();
                 cx.focus_view(&new_menu);
                 *menu.borrow_mut() = Some(new_menu);
 
                 *position.borrow_mut() = if attach.is_some() && child_layout_id.is_some() {
-                    attach
-                        .unwrap()
-                        .corner(cx.layout_bounds(child_layout_id.unwrap()))
+                    attach.unwrap().corner(child_bounds)
                 } else {
                     cx.mouse_position()
                 };
-                cx.notify();
+                cx.refresh();
             }
         });
     }

crates/ui/src/components/stack.rs 🔗

@@ -4,12 +4,12 @@ use crate::StyledExt;
 
 /// Horizontally stacks elements. Sets `flex()`, `flex_row()`, `items_center()`
 #[track_caller]
-pub fn h_stack() -> Div {
+pub fn h_flex() -> Div {
     div().h_flex()
 }
 
 /// Vertically stacks elements. Sets `flex()`, `flex_col()`
 #[track_caller]
-pub fn v_stack() -> Div {
+pub fn v_flex() -> Div {
     div().v_flex()
 }

crates/ui/src/components/stories/checkbox.rs 🔗

@@ -2,7 +2,7 @@ use gpui::{Render, ViewContext};
 use story::Story;
 
 use crate::prelude::*;
-use crate::{h_stack, Checkbox};
+use crate::{h_flex, Checkbox};
 
 pub struct CheckboxStory;
 
@@ -12,7 +12,7 @@ impl Render for CheckboxStory {
             .child(Story::title_for::<Checkbox>())
             .child(Story::label("Default"))
             .child(
-                h_stack()
+                h_flex()
                     .p_2()
                     .gap_2()
                     .rounded_md()
@@ -27,7 +27,7 @@ impl Render for CheckboxStory {
             )
             .child(Story::label("Disabled"))
             .child(
-                h_stack()
+                h_flex()
                     .p_2()
                     .gap_2()
                     .rounded_md()

crates/ui/src/components/stories/icon_button.rs 🔗

@@ -117,55 +117,5 @@ impl Render for IconButtonStory {
         )
         .children(vec![StorySection::new().children(buttons)])
         .into_element()
-
-        // Story::container()
-        //     .child(Story::title_for::<IconButton>())
-        //     .child(Story::label("Default"))
-        //     .child(div().w_8().child(IconButton::new("icon_a", Icon::Hash)))
-        //     .child(Story::label("Selected"))
-        //     .child(
-        //         div()
-        //             .w_8()
-        //             .child(IconButton::new("icon_a", Icon::Hash).selected(true)),
-        //     )
-        //     .child(Story::label("Selected with `selected_icon`"))
-        //     .child(
-        //         div().w_8().child(
-        //             IconButton::new("icon_a", Icon::AudioOn)
-        //                 .selected(true)
-        //                 .selected_icon(Icon::AudioOff),
-        //         ),
-        //     )
-        //     .child(Story::label("Disabled"))
-        //     .child(
-        //         div()
-        //             .w_8()
-        //             .child(IconButton::new("icon_a", Icon::Hash).disabled(true)),
-        //     )
-        //     .child(Story::label("With `on_click`"))
-        //     .child(
-        //         div()
-        //             .w_8()
-        //             .child(
-        //                 IconButton::new("with_on_click", Icon::Ai).on_click(|_event, _cx| {
-        //                     println!("Clicked!");
-        //                 }),
-        //             ),
-        //     )
-        //     .child(Story::label("With `tooltip`"))
-        //     .child(
-        //         div().w_8().child(
-        //             IconButton::new("with_tooltip", Icon::MessageBubbles)
-        //                 .tooltip(|cx| Tooltip::text("Open messages", cx)),
-        //         ),
-        //     )
-        //     .child(Story::label("Selected with `tooltip`"))
-        //     .child(
-        //         div().w_8().child(
-        //             IconButton::new("selected_with_tooltip", Icon::InlayHint)
-        //                 .selected(true)
-        //                 .tooltip(|cx| Tooltip::text("Toggle inlay hints", cx)),
-        //         ),
-        //     )
     }
 }

crates/ui/src/components/stories/list_item.rs 🔗

@@ -60,7 +60,7 @@ impl Render for ListItemStory {
                 ListItem::new("with_end_hover_slot")
                     .child("Hello, world!")
                     .end_slot(
-                        h_stack()
+                        h_flex()
                             .gap_2()
                             .child(Avatar::new(SharedUrl::from(
                                 "https://avatars.githubusercontent.com/u/1789?v=4",

crates/ui/src/components/stories/tab.rs 🔗

@@ -3,7 +3,7 @@ use std::cmp::Ordering;
 use gpui::Render;
 use story::Story;
 
-use crate::{prelude::*, TabPosition};
+use crate::{prelude::*, IconButtonShape, TabPosition};
 use crate::{Indicator, Tab};
 
 pub struct TabStory;
@@ -13,10 +13,10 @@ impl Render for TabStory {
         Story::container()
             .child(Story::title_for::<Tab>())
             .child(Story::label("Default"))
-            .child(h_stack().child(Tab::new("tab_1").child("Tab 1")))
+            .child(h_flex().child(Tab::new("tab_1").child("Tab 1")))
             .child(Story::label("With indicator"))
             .child(
-                h_stack().child(
+                h_flex().child(
                     Tab::new("tab_1")
                         .start_slot(Indicator::dot().color(Color::Warning))
                         .child("Tab 1"),
@@ -24,10 +24,11 @@ impl Render for TabStory {
             )
             .child(Story::label("With close button"))
             .child(
-                h_stack().child(
+                h_flex().child(
                     Tab::new("tab_1")
                         .end_slot(
                             IconButton::new("close_button", IconName::Close)
+                                .shape(IconButtonShape::Square)
                                 .icon_color(Color::Muted)
                                 .size(ButtonSize::None)
                                 .icon_size(IconSize::XSmall),
@@ -37,13 +38,13 @@ impl Render for TabStory {
             )
             .child(Story::label("List of tabs"))
             .child(
-                h_stack()
+                h_flex()
                     .child(Tab::new("tab_1").child("Tab 1"))
                     .child(Tab::new("tab_2").child("Tab 2")),
             )
             .child(Story::label("List of tabs with first tab selected"))
             .child(
-                h_stack()
+                h_flex()
                     .child(
                         Tab::new("tab_1")
                             .selected(true)
@@ -64,7 +65,7 @@ impl Render for TabStory {
             )
             .child(Story::label("List of tabs with last tab selected"))
             .child(
-                h_stack()
+                h_flex()
                     .child(
                         Tab::new("tab_1")
                             .position(TabPosition::First)
@@ -89,7 +90,7 @@ impl Render for TabStory {
             )
             .child(Story::label("List of tabs with second tab selected"))
             .child(
-                h_stack()
+                h_flex()
                     .child(
                         Tab::new("tab_1")
                             .position(TabPosition::First)

crates/ui/src/components/stories/tab_bar.rs 🔗

@@ -35,7 +35,7 @@ impl Render for TabBarStory {
             .child(Story::title_for::<TabBar>())
             .child(Story::label("Default"))
             .child(
-                h_stack().child(
+                h_flex().child(
                     TabBar::new("tab_bar_1")
                         .start_child(
                             IconButton::new("navigate_backward", IconName::ArrowLeft)

crates/ui/src/components/stories/toggle_button.rs 🔗

@@ -25,7 +25,7 @@ impl Render for ToggleButtonStory {
             StorySection::new().child(
                 StoryItem::new(
                     "Toggle button group",
-                    h_stack()
+                    h_flex()
                         .child(
                             ToggleButton::new(1, "Apple")
                                 .style(ButtonStyle::Filled)
@@ -59,7 +59,7 @@ impl Render for ToggleButtonStory {
             StorySection::new().child(
                 StoryItem::new(
                     "Toggle button group with selection",
-                    h_stack()
+                    h_flex()
                         .child(
                             ToggleButton::new(1, "Apple")
                                 .style(ButtonStyle::Filled)

crates/ui/src/components/tab.rs 🔗

@@ -48,7 +48,9 @@ impl Tab {
         }
     }
 
-    pub const HEIGHT_IN_REMS: f32 = 30. / 16.;
+    pub const CONTAINER_HEIGHT_IN_REMS: f32 = 29. / 16.;
+
+    const CONTENT_HEIGHT_IN_REMS: f32 = 28. / 16.;
 
     pub fn position(mut self, position: TabPosition) -> Self {
         self.position = position;
@@ -111,7 +113,7 @@ impl RenderOnce for Tab {
         };
 
         self.div
-            .h(rems(Self::HEIGHT_IN_REMS))
+            .h(rems(Self::CONTAINER_HEIGHT_IN_REMS))
             .bg(tab_bg)
             .border_color(cx.theme().colors().border)
             .map(|this| match self.position {
@@ -135,17 +137,17 @@ impl RenderOnce for Tab {
             })
             .cursor_pointer()
             .child(
-                h_stack()
+                h_flex()
                     .group("")
                     .relative()
-                    .h_full()
+                    .h(rems(Self::CONTENT_HEIGHT_IN_REMS))
                     .px_5()
                     .gap_1()
                     .text_color(text_color)
                     // .hover(|style| style.bg(tab_hover_bg))
                     // .active(|style| style.bg(tab_active_bg))
                     .child(
-                        h_stack()
+                        h_flex()
                             .w_3()
                             .h_3()
                             .justify_center()
@@ -157,7 +159,7 @@ impl RenderOnce for Tab {
                             .children(self.start_slot),
                     )
                     .child(
-                        h_stack()
+                        h_flex()
                             .w_3()
                             .h_3()
                             .justify_center()

crates/ui/src/components/tab_bar.rs 🔗

@@ -90,7 +90,7 @@ impl ParentElement for TabBar {
 
 impl RenderOnce for TabBar {
     fn render(self, cx: &mut WindowContext) -> impl IntoElement {
-        const HEIGHT_IN_REMS: f32 = 30. / 16.;
+        const HEIGHT_IN_REMS: f32 = 29. / 16.;
 
         div()
             .id(self.id)
@@ -102,7 +102,7 @@ impl RenderOnce for TabBar {
             .bg(cx.theme().colors().tab_bar_background)
             .when(!self.start_children.is_empty(), |this| {
                 this.child(
-                    h_stack()
+                    h_flex()
                         .flex_none()
                         .gap_1()
                         .px_1()
@@ -129,7 +129,7 @@ impl RenderOnce for TabBar {
                             .border_color(cx.theme().colors().border),
                     )
                     .child(
-                        h_stack()
+                        h_flex()
                             .id("tabs")
                             .z_index(2)
                             .flex_grow()
@@ -142,7 +142,7 @@ impl RenderOnce for TabBar {
             )
             .when(!self.end_children.is_empty(), |this| {
                 this.child(
-                    h_stack()
+                    h_flex()
                         .flex_none()
                         .gap_1()
                         .px_1()

crates/ui/src/components/tooltip.rs 🔗

@@ -3,7 +3,7 @@ use settings::Settings;
 use theme::ThemeSettings;
 
 use crate::prelude::*;
-use crate::{h_stack, v_stack, Color, KeyBinding, Label, LabelSize, StyledExt};
+use crate::{h_flex, v_flex, Color, KeyBinding, Label, LabelSize, StyledExt};
 
 pub struct Tooltip {
     title: SharedString,
@@ -73,7 +73,7 @@ impl Render for Tooltip {
         overlay().child(
             // padding to avoid mouse cursor
             div().pl_2().pt_2p5().child(
-                v_stack()
+                v_flex()
                     .elevation_2(cx)
                     .font(ui_font)
                     .text_ui()
@@ -81,7 +81,7 @@ impl Render for Tooltip {
                     .py_1()
                     .px_2()
                     .child(
-                        h_stack()
+                        h_flex()
                             .gap_4()
                             .child(self.title.clone())
                             .when_some(self.key_binding.clone(), |this, key_binding| {

crates/ui/src/prelude.rs 🔗

@@ -13,7 +13,7 @@ pub use crate::fixed::*;
 pub use crate::selectable::*;
 pub use crate::styles::{vh, vw};
 pub use crate::visible_on_hover::*;
-pub use crate::{h_stack, v_stack};
+pub use crate::{h_flex, v_flex};
 pub use crate::{Button, ButtonSize, ButtonStyle, IconButton, SelectableButton};
 pub use crate::{ButtonCommon, Color, StyledExt};
 pub use crate::{Headline, HeadlineSize};

crates/vcs_menu/src/lib.rs 🔗

@@ -9,7 +9,7 @@ use gpui::{
 use picker::{Picker, PickerDelegate};
 use std::{ops::Not, sync::Arc};
 use ui::{
-    h_stack, v_stack, Button, ButtonCommon, Clickable, HighlightedLabel, Label, LabelCommon,
+    h_flex, v_flex, Button, ButtonCommon, Clickable, HighlightedLabel, Label, LabelCommon,
     LabelSize, ListItem, ListItemSpacing, Selectable,
 };
 use util::ResultExt;
@@ -65,7 +65,7 @@ impl FocusableView for BranchList {
 
 impl Render for BranchList {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
-        v_stack()
+        v_flex()
             .w(rems(self.rem_width))
             .child(self.picker.clone())
             .on_mouse_down_out(cx.listener(|this, _, cx| {
@@ -290,7 +290,7 @@ impl PickerDelegate for BranchListDelegate {
     }
     fn render_header(&self, _: &mut ViewContext<Picker<Self>>) -> Option<AnyElement> {
         let label = if self.last_query.is_empty() {
-            h_stack()
+            h_flex()
                 .ml_3()
                 .child(Label::new("Recent branches").size(LabelSize::Small))
         } else {
@@ -298,7 +298,7 @@ impl PickerDelegate for BranchListDelegate {
                 let suffix = if self.matches.len() == 1 { "" } else { "es" };
                 Label::new(format!("{} match{}", self.matches.len(), suffix)).size(LabelSize::Small)
             });
-            h_stack()
+            h_flex()
                 .px_3()
                 .h_full()
                 .justify_between()
@@ -313,7 +313,7 @@ impl PickerDelegate for BranchListDelegate {
         }
 
         Some(
-            h_stack().mr_3().pb_2().child(h_stack().w_full()).child(
+            h_flex().mr_3().pb_2().child(h_flex().w_full()).child(
             Button::new("branch-picker-create-branch-button", "Create branch").on_click(
                 cx.listener(|_, _, cx| {
                     cx.spawn(|picker, mut cx| async move {

crates/welcome/src/base_keymap_picker.rs 🔗

@@ -62,7 +62,7 @@ impl BaseKeymapSelector {
 
 impl Render for BaseKeymapSelector {
     fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
-        v_stack().w(rems(34.)).child(self.picker.clone())
+        v_flex().w(rems(34.)).child(self.picker.clone())
     }
 }
 

crates/welcome/src/welcome.rs 🔗

@@ -60,8 +60,8 @@ pub struct WelcomePage {
 
 impl Render for WelcomePage {
     fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
-        h_stack().full().track_focus(&self.focus_handle).child(
-            v_stack()
+        h_flex().full().track_focus(&self.focus_handle).child(
+            v_flex()
                 .w_96()
                 .gap_4()
                 .mx_auto()
@@ -74,19 +74,19 @@ impl Render for WelcomePage {
                         .mx_auto(),
                 )
                 .child(
-                    h_stack()
+                    h_flex()
                         .justify_center()
                         .child(Label::new("Code at the speed of thought")),
                 )
                 .child(
-                    v_stack()
+                    v_flex()
                         .gap_2()
                         .child(
                             Button::new("choose-theme", "Choose a theme")
                                 .full_width()
                                 .on_click(cx.listener(|this, _, cx| {
                                     this.telemetry
-                                        .report_app_event("welcome page: change theme");
+                                        .report_app_event("welcome page: change theme".to_string());
                                     this.workspace
                                         .update(cx, |workspace, cx| {
                                             theme_selector::toggle(
@@ -102,8 +102,9 @@ impl Render for WelcomePage {
                             Button::new("choose-keymap", "Choose a keymap")
                                 .full_width()
                                 .on_click(cx.listener(|this, _, cx| {
-                                    this.telemetry
-                                        .report_app_event("welcome page: change keymap");
+                                    this.telemetry.report_app_event(
+                                        "welcome page: change keymap".to_string(),
+                                    );
                                     this.workspace
                                         .update(cx, |workspace, cx| {
                                             base_keymap_picker::toggle(
@@ -119,7 +120,8 @@ impl Render for WelcomePage {
                             Button::new("install-cli", "Install the CLI")
                                 .full_width()
                                 .on_click(cx.listener(|this, _, cx| {
-                                    this.telemetry.report_app_event("welcome page: install cli");
+                                    this.telemetry
+                                        .report_app_event("welcome page: install cli".to_string());
                                     cx.app_mut()
                                         .spawn(
                                             |cx| async move { install_cli::install_cli(&cx).await },
@@ -129,7 +131,7 @@ impl Render for WelcomePage {
                         ),
                 )
                 .child(
-                    v_stack()
+                    v_flex()
                         .p_3()
                         .gap_2()
                         .bg(cx.theme().colors().elevated_surface_background)
@@ -137,7 +139,7 @@ impl Render for WelcomePage {
                         .border_color(cx.theme().colors().border)
                         .rounded_md()
                         .child(
-                            h_stack()
+                            h_flex()
                                 .gap_2()
                                 .child(
                                     Checkbox::new(
@@ -150,8 +152,9 @@ impl Render for WelcomePage {
                                     )
                                     .on_click(cx.listener(
                                         move |this, selection, cx| {
-                                            this.telemetry
-                                                .report_app_event("welcome page: toggle vim");
+                                            this.telemetry.report_app_event(
+                                                "welcome page: toggle vim".to_string(),
+                                            );
                                             this.update_settings::<VimModeSetting>(
                                                 selection,
                                                 cx,
@@ -163,7 +166,7 @@ impl Render for WelcomePage {
                                 .child(Label::new("Enable vim mode")),
                         )
                         .child(
-                            h_stack()
+                            h_flex()
                                 .gap_2()
                                 .child(
                                     Checkbox::new(
@@ -177,7 +180,7 @@ impl Render for WelcomePage {
                                     .on_click(cx.listener(
                                         move |this, selection, cx| {
                                             this.telemetry.report_app_event(
-                                                "welcome page: toggle metric telemetry",
+                                                "welcome page: toggle metric telemetry".to_string(),
                                             );
                                             this.update_settings::<TelemetrySettings>(
                                                 selection,
@@ -201,7 +204,7 @@ impl Render for WelcomePage {
                                 .child(Label::new("Send anonymous usage data")),
                         )
                         .child(
-                            h_stack()
+                            h_flex()
                                 .gap_2()
                                 .child(
                                     Checkbox::new(
@@ -215,7 +218,8 @@ impl Render for WelcomePage {
                                     .on_click(cx.listener(
                                         move |this, selection, cx| {
                                             this.telemetry.report_app_event(
-                                                "welcome page: toggle diagnostic telemetry",
+                                                "welcome page: toggle diagnostic telemetry"
+                                                    .to_string(),
                                             );
                                             this.update_settings::<TelemetrySettings>(
                                                 selection,
@@ -247,7 +251,8 @@ impl WelcomePage {
     pub fn new(workspace: &Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
         let this = cx.new_view(|cx| {
             cx.on_release(|this: &mut Self, _, _| {
-                this.telemetry.report_app_event("welcome page: close");
+                this.telemetry
+                    .report_app_event("welcome page: close".to_string());
             })
             .detach();
 
@@ -306,6 +311,10 @@ impl Item for WelcomePage {
             .into_any_element()
     }
 
+    fn telemetry_event_text(&self) -> Option<&'static str> {
+        Some("welcome page")
+    }
+
     fn show_toolbar(&self) -> bool {
         false
     }

crates/workspace/src/dock.rs 🔗

@@ -7,14 +7,14 @@ use gpui::{
 };
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
+use settings::SettingsStore;
 use std::sync::Arc;
-use ui::{h_stack, ContextMenu, IconButton, Tooltip};
+use ui::{h_flex, ContextMenu, IconButton, Tooltip};
 use ui::{prelude::*, right_click_menu};
 
 const RESIZE_HANDLE_SIZE: Pixels = Pixels(6.);
 
 pub enum PanelEvent {
-    ChangePosition,
     ZoomIn,
     ZoomOut,
     Activate,
@@ -177,7 +177,7 @@ impl DockPosition {
 
 struct PanelEntry {
     panel: Arc<dyn PanelHandle>,
-    _subscriptions: [Subscription; 2],
+    _subscriptions: [Subscription; 3],
 }
 
 pub struct PanelButtons {
@@ -321,9 +321,15 @@ impl Dock {
     ) {
         let subscriptions = [
             cx.observe(&panel, |_, _, cx| cx.notify()),
-            cx.subscribe(&panel, move |this, panel, event, cx| match event {
-                PanelEvent::ChangePosition => {
+            cx.observe_global::<SettingsStore>({
+                let workspace = workspace.clone();
+                let panel = panel.clone();
+
+                move |this, cx| {
                     let new_position = panel.read(cx).position(cx);
+                    if new_position == this.position {
+                        return;
+                    }
 
                     let Ok(new_dock) = workspace.update(cx, |workspace, cx| {
                         if panel.is_zoomed(cx) {
@@ -354,6 +360,8 @@ impl Dock {
                         }
                     });
                 }
+            }),
+            cx.subscribe(&panel, move |this, panel, event, cx| match event {
                 PanelEvent::ZoomIn => {
                     this.set_panel_zoomed(&panel.to_any(), true, cx);
                     if !panel.focus_handle(cx).contains_focused(cx) {
@@ -575,7 +583,7 @@ impl Render for Dock {
                             Axis::Horizontal => this.min_w(size).h_full(),
                             Axis::Vertical => this.min_h(size).w_full(),
                         })
-                        .child(entry.panel.to_any()),
+                        .child(entry.panel.to_any().cached()),
                 )
                 .child(handle)
         } else {
@@ -674,7 +682,7 @@ impl Render for PanelButtons {
                 )
             });
 
-        h_stack().gap_0p5().children(buttons)
+        h_flex().gap_0p5().children(buttons)
     }
 }
 
@@ -737,7 +745,7 @@ pub mod test {
 
         fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
             self.position = position;
-            cx.emit(PanelEvent::ChangePosition);
+            cx.update_global::<SettingsStore, _>(|_, _| {});
         }
 
         fn size(&self, _: &WindowContext) -> Pixels {

crates/workspace/src/item.rs 🔗

@@ -114,6 +114,8 @@ pub trait Item: FocusableView + EventEmitter<Self::Event> {
     }
     fn tab_content(&self, detail: Option<usize>, selected: bool, cx: &WindowContext) -> AnyElement;
 
+    fn telemetry_event_text(&self) -> Option<&'static str>;
+
     /// (model id, Item)
     fn for_each_project_item(
         &self,
@@ -225,6 +227,7 @@ pub trait ItemHandle: 'static + Send {
     fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString>;
     fn tab_description(&self, detail: usize, cx: &AppContext) -> Option<SharedString>;
     fn tab_content(&self, detail: Option<usize>, selected: bool, cx: &WindowContext) -> AnyElement;
+    fn telemetry_event_text(&self, cx: &WindowContext) -> Option<&'static str>;
     fn dragged_tab_content(&self, detail: Option<usize>, cx: &WindowContext) -> AnyElement;
     fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
     fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>;
@@ -313,6 +316,10 @@ impl<T: Item> ItemHandle for View<T> {
         self.read(cx).tab_tooltip_text(cx)
     }
 
+    fn telemetry_event_text(&self, cx: &WindowContext) -> Option<&'static str> {
+        self.read(cx).telemetry_event_text()
+    }
+
     fn tab_description(&self, detail: usize, cx: &AppContext) -> Option<SharedString> {
         self.read(cx).tab_description(detail, cx)
     }
@@ -809,27 +816,6 @@ pub mod test {
         Edit,
     }
 
-    // impl Clone for TestItem {
-    //     fn clone(&self) -> Self {
-    //         Self {
-    //             state: self.state.clone(),
-    //             label: self.label.clone(),
-    //             save_count: self.save_count,
-    //             save_as_count: self.save_as_count,
-    //             reload_count: self.reload_count,
-    //             is_dirty: self.is_dirty,
-    //             is_singleton: self.is_singleton,
-    //             has_conflict: self.has_conflict,
-    //             project_items: self.project_items.clone(),
-    //             nav_history: None,
-    //             tab_descriptions: None,
-    //             tab_detail: Default::default(),
-    //             workspace_id: self.workspace_id,
-    //             focus_handle: self.focus_handle.clone(),
-    //         }
-    //     }
-    // }
-
     impl TestProjectItem {
         pub fn new(id: u64, path: &str, cx: &mut AppContext) -> Model<Self> {
             let entry_id = Some(ProjectEntryId::from_proto(id));
@@ -943,6 +929,10 @@ pub mod test {
             })
         }
 
+        fn telemetry_event_text(&self) -> Option<&'static str> {
+            None
+        }
+
         fn tab_content(
             &self,
             detail: Option<usize>,

crates/workspace/src/modal_layer.rs 🔗

@@ -2,7 +2,7 @@ use gpui::{
     div, prelude::*, px, AnyView, DismissEvent, FocusHandle, ManagedView, Render, Subscription,
     View, ViewContext, WindowContext,
 };
-use ui::{h_stack, v_stack};
+use ui::{h_flex, v_flex};
 
 pub trait ModalView: ManagedView {
     fn on_before_dismiss(&mut self, _: &mut ViewContext<Self>) -> bool {
@@ -120,7 +120,7 @@ impl Render for ModalLayer {
             .left_0()
             .z_index(169)
             .child(
-                v_stack()
+                v_flex()
                     .h(px(0.0))
                     .top_20()
                     .flex()
@@ -128,7 +128,7 @@ impl Render for ModalLayer {
                     .items_center()
                     .track_focus(&active_modal.focus_handle)
                     .child(
-                        h_stack()
+                        h_flex()
                             .on_mouse_down_out(cx.listener(|this, _, cx| {
                                 this.hide_modal(cx);
                             }))

crates/workspace/src/notifications.rs 🔗

@@ -173,7 +173,7 @@ pub mod simple_message_notification {
     };
     use std::sync::Arc;
     use ui::prelude::*;
-    use ui::{h_stack, v_stack, Button, Icon, IconName, Label, StyledExt};
+    use ui::{h_flex, v_flex, Button, Icon, IconName, Label, StyledExt};
 
     pub struct MessageNotification {
         message: SharedString,
@@ -218,11 +218,11 @@ pub mod simple_message_notification {
 
     impl Render for MessageNotification {
         fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
-            v_stack()
+            v_flex()
                 .elevation_3(cx)
                 .p_4()
                 .child(
-                    h_stack()
+                    h_flex()
                         .justify_between()
                         .child(div().max_w_80().child(Label::new(self.message.clone())))
                         .child(

crates/workspace/src/pane.rs 🔗

@@ -32,10 +32,10 @@ use std::{
 use theme::ThemeSettings;
 
 use ui::{
-    prelude::*, right_click_menu, ButtonSize, Color, IconButton, IconName, IconSize, Indicator,
-    Label, Tab, TabBar, TabPosition, Tooltip,
+    prelude::*, right_click_menu, ButtonSize, Color, IconButton, IconButtonShape, IconName,
+    IconSize, Indicator, Label, Tab, TabBar, TabPosition, Tooltip,
 };
-use ui::{v_stack, ContextMenu};
+use ui::{v_flex, ContextMenu};
 use util::{maybe, truncate_and_remove_front, ResultExt};
 
 #[derive(PartialEq, Clone, Copy, Deserialize, Debug)]
@@ -60,24 +60,6 @@ pub enum SaveIntent {
 #[derive(Clone, Deserialize, PartialEq, Debug)]
 pub struct ActivateItem(pub usize);
 
-// #[derive(Clone, PartialEq)]
-// pub struct CloseItemById {
-//     pub item_id: usize,
-//     pub pane: WeakView<Pane>,
-// }
-
-// #[derive(Clone, PartialEq)]
-// pub struct CloseItemsToTheLeftById {
-//     pub item_id: usize,
-//     pub pane: WeakView<Pane>,
-// }
-
-// #[derive(Clone, PartialEq)]
-// pub struct CloseItemsToTheRightById {
-//     pub item_id: usize,
-//     pub pane: WeakView<Pane>,
-// }
-
 #[derive(Clone, PartialEq, Debug, Deserialize, Default)]
 #[serde(rename_all = "camelCase")]
 pub struct CloseActiveItem {
@@ -237,8 +219,8 @@ pub struct NavigationEntry {
 #[derive(Clone)]
 pub struct DraggedTab {
     pub pane: View<Pane>,
+    pub item: Box<dyn ItemHandle>,
     pub ix: usize,
-    pub item_id: EntityId,
     pub detail: usize,
     pub is_active: bool,
 }
@@ -289,7 +271,7 @@ impl Pane {
             custom_drop_handle: None,
             can_split: true,
             render_tab_bar_buttons: Rc::new(move |pane, cx| {
-                h_stack()
+                h_flex()
                     .gap_2()
                     .child(
                         IconButton::new("plus", IconName::Plus)
@@ -1226,125 +1208,6 @@ impl Pane {
         cx.emit(Event::Split(direction));
     }
 
-    //     fn deploy_split_menu(&mut self, cx: &mut ViewContext<Self>) {
-    //         self.tab_bar_context_menu.handle.update(cx, |menu, cx| {
-    //             menu.toggle(
-    //                 Default::default(),
-    //                 AnchorCorner::TopRight,
-    //                 vec![
-    //                     ContextMenuItem::action("Split Right", SplitRight),
-    //                     ContextMenuItem::action("Split Left", SplitLeft),
-    //                     ContextMenuItem::action("Split Up", SplitUp),
-    //                     ContextMenuItem::action("Split Down", SplitDown),
-    //                 ],
-    //                 cx,
-    //             );
-    //         });
-
-    //         self.tab_bar_context_menu.kind = TabBarContextMenuKind::Split;
-    //     }
-
-    //     fn deploy_new_menu(&mut self, cx: &mut ViewContext<Self>) {
-    //         self.tab_bar_context_menu.handle.update(cx, |menu, cx| {
-    //             menu.toggle(
-    //                 Default::default(),
-    //                 AnchorCorner::TopRight,
-    //                 vec![
-    //                     ContextMenuItem::action("New File", NewFile),
-    //                     ContextMenuItem::action("New Terminal", NewCenterTerminal),
-    //                     ContextMenuItem::action("New Search", NewSearch),
-    //                 ],
-    //                 cx,
-    //             );
-    //         });
-
-    //         self.tab_bar_context_menu.kind = TabBarContextMenuKind::New;
-    //     }
-
-    //     fn deploy_tab_context_menu(
-    //         &mut self,
-    //         position: Vector2F,
-    //         target_item_id: usize,
-    //         cx: &mut ViewContext<Self>,
-    //     ) {
-    //         let active_item_id = self.items[self.active_item_index].id();
-    //         let is_active_item = target_item_id == active_item_id;
-    //         let target_pane = cx.weak_handle();
-
-    //         // The `CloseInactiveItems` action should really be called "CloseOthers" and the behaviour should be dynamically based on the tab the action is ran on.  Currently, this is a weird action because you can run it on a non-active tab and it will close everything by the actual active tab
-
-    //         self.tab_context_menu.update(cx, |menu, cx| {
-    //             menu.show(
-    //                 position,
-    //                 AnchorCorner::TopLeft,
-    //                 if is_active_item {
-    //                     vec![
-    //                         ContextMenuItem::action(
-    //                             "Close Active Item",
-    //                             CloseActiveItem { save_intent: None },
-    //                         ),
-    //                         ContextMenuItem::action("Close Inactive Items", CloseInactiveItems),
-    //                         ContextMenuItem::action("Close Clean Items", CloseCleanItems),
-    //                         ContextMenuItem::action("Close Items To The Left", CloseItemsToTheLeft),
-    //                         ContextMenuItem::action("Close Items To The Right", CloseItemsToTheRight),
-    //                         ContextMenuItem::action(
-    //                             "Close All Items",
-    //                             CloseAllItems { save_intent: None },
-    //                         ),
-    //                     ]
-    //                 } else {
-    //                     // In the case of the user right clicking on a non-active tab, for some item-closing commands, we need to provide the id of the tab, for the others, we can reuse the existing command.
-    //                     vec![
-    //                         ContextMenuItem::handler("Close Inactive Item", {
-    //                             let pane = target_pane.clone();
-    //                             move |cx| {
-    //                                 if let Some(pane) = pane.upgrade(cx) {
-    //                                     pane.update(cx, |pane, cx| {
-    //                                         pane.close_item_by_id(
-    //                                             target_item_id,
-    //                                             SaveIntent::Close,
-    //                                             cx,
-    //                                         )
-    //                                         .detach_and_log_err(cx);
-    //                                     })
-    //                                 }
-    //                             }
-    //                         }),
-    //                         ContextMenuItem::action("Close Inactive Items", CloseInactiveItems),
-    //                         ContextMenuItem::action("Close Clean Items", CloseCleanItems),
-    //                         ContextMenuItem::handler("Close Items To The Left", {
-    //                             let pane = target_pane.clone();
-    //                             move |cx| {
-    //                                 if let Some(pane) = pane.upgrade(cx) {
-    //                                     pane.update(cx, |pane, cx| {
-    //                                         pane.close_items_to_the_left_by_id(target_item_id, cx)
-    //                                             .detach_and_log_err(cx);
-    //                                     })
-    //                                 }
-    //                             }
-    //                         }),
-    //                         ContextMenuItem::handler("Close Items To The Right", {
-    //                             let pane = target_pane.clone();
-    //                             move |cx| {
-    //                                 if let Some(pane) = pane.upgrade(cx) {
-    //                                     pane.update(cx, |pane, cx| {
-    //                                         pane.close_items_to_the_right_by_id(target_item_id, cx)
-    //                                             .detach_and_log_err(cx);
-    //                                     })
-    //                                 }
-    //                             }
-    //                         }),
-    //                         ContextMenuItem::action(
-    //                             "Close All Items",
-    //                             CloseAllItems { save_intent: None },
-    //                         ),
-    //                     ]
-    //                 },
-    //                 cx,
-    //             );
-    //         });
-    //     }
-
     pub fn toolbar(&self) -> &View<Toolbar> {
         &self.toolbar
     }
@@ -1447,9 +1310,9 @@ impl Pane {
             )
             .on_drag(
                 DraggedTab {
+                    item: item.boxed_clone(),
                     pane: cx.view().clone(),
                     detail,
-                    item_id,
                     is_active,
                     ix,
                 },
@@ -1478,6 +1341,7 @@ impl Pane {
             .start_slot::<Indicator>(indicator)
             .end_slot(
                 IconButton::new("close tab", IconName::Close)
+                    .shape(IconButtonShape::Square)
                     .icon_color(Color::Muted)
                     .size(ButtonSize::None)
                     .icon_size(IconSize::XSmall)
@@ -1580,7 +1444,7 @@ impl Pane {
             .track_scroll(self.tab_bar_scroll_handle.clone())
             .when(self.display_nav_history_buttons, |tab_bar| {
                 tab_bar.start_child(
-                    h_stack()
+                    h_flex()
                         .gap_2()
                         .child(
                             IconButton::new("navigate_backward", IconName::ArrowLeft)
@@ -1739,7 +1603,7 @@ impl Pane {
         }
         let mut to_pane = cx.view().clone();
         let split_direction = self.drag_split_direction;
-        let item_id = dragged_tab.item_id;
+        let item_id = dragged_tab.item.item_id();
         let from_pane = dragged_tab.pane.clone();
         self.workspace
             .update(cx, |_, cx| {
@@ -1854,7 +1718,7 @@ impl FocusableView for Pane {
 
 impl Render for Pane {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
-        v_stack()
+        v_flex()
             .key_context("Pane")
             .track_focus(&self.focus_handle)
             .size_full()
@@ -2739,8 +2603,7 @@ mod tests {
 impl Render for DraggedTab {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
         let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
-        let item = &self.pane.read(cx).items[self.ix];
-        let label = item.tab_content(Some(self.detail), false, cx);
+        let label = self.item.tab_content(Some(self.detail), false, cx);
         Tab::new("")
             .selected(self.is_active)
             .child(label)

crates/workspace/src/pane_group.rs 🔗

@@ -3,8 +3,8 @@ use anyhow::{anyhow, Result};
 use call::{ActiveCall, ParticipantLocation};
 use collections::HashMap;
 use gpui::{
-    point, size, AnyWeakView, Axis, Bounds, Entity as _, IntoElement, Model, Pixels, Point, View,
-    ViewContext,
+    point, size, AnyView, AnyWeakView, Axis, Bounds, Entity as _, IntoElement, Model, Pixels,
+    Point, View, ViewContext,
 };
 use parking_lot::Mutex;
 use project::Project;
@@ -244,7 +244,7 @@ impl Member {
                     .relative()
                     .flex_1()
                     .size_full()
-                    .child(pane.clone())
+                    .child(AnyView::from(pane.clone()).cached())
                     .when_some(leader_border, |this, color| {
                         this.child(
                             div()
@@ -701,7 +701,7 @@ mod element {
             workspace
                 .update(cx, |this, cx| this.schedule_serialize(cx))
                 .log_err();
-            cx.notify();
+            cx.refresh();
         }
 
         fn push_handle(
@@ -757,7 +757,7 @@ mod element {
                                 workspace
                                     .update(cx, |this, cx| this.schedule_serialize(cx))
                                     .log_err();
-                                cx.notify();
+                                cx.refresh();
                             }
                         }
                     }

crates/workspace/src/shared_screen.rs 🔗

@@ -12,7 +12,7 @@ use gpui::{
     WindowContext,
 };
 use std::sync::{Arc, Weak};
-use ui::{h_stack, prelude::*, Icon, IconName, Label};
+use ui::{h_flex, prelude::*, Icon, IconName, Label};
 
 pub enum Event {
     Close,
@@ -98,7 +98,7 @@ impl Item for SharedScreen {
         selected: bool,
         _: &WindowContext<'_>,
     ) -> gpui::AnyElement {
-        h_stack()
+        h_flex()
             .gap_1()
             .child(Icon::new(IconName::Screen))
             .child(
@@ -111,6 +111,10 @@ impl Item for SharedScreen {
             .into_any()
     }
 
+    fn telemetry_event_text(&self) -> Option<&'static str> {
+        None
+    }
+
     fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext<Self>) {
         self.nav_history = Some(history);
     }

crates/workspace/src/status_bar.rs 🔗

@@ -4,7 +4,7 @@ use gpui::{
     WindowContext,
 };
 use std::any::TypeId;
-use ui::{h_stack, prelude::*};
+use ui::{h_flex, prelude::*};
 use util::ResultExt;
 
 pub trait StatusItemView: Render {
@@ -50,14 +50,14 @@ impl Render for StatusBar {
 
 impl StatusBar {
     fn render_left_tools(&self, _: &mut ViewContext<Self>) -> impl IntoElement {
-        h_stack()
+        h_flex()
             .items_center()
             .gap_2()
             .children(self.left_items.iter().map(|item| item.to_any()))
     }
 
     fn render_right_tools(&self, _: &mut ViewContext<Self>) -> impl IntoElement {
-        h_stack()
+        h_flex()
             .items_center()
             .gap_2()
             .children(self.right_items.iter().rev().map(|item| item.to_any()))

crates/workspace/src/toolbar.rs 🔗

@@ -4,7 +4,7 @@ use gpui::{
     WindowContext,
 };
 use ui::prelude::*;
-use ui::{h_stack, v_stack};
+use ui::{h_flex, v_flex};
 
 pub enum ToolbarItemEvent {
     ChangeLocation(ToolbarItemLocation),
@@ -103,18 +103,18 @@ impl Render for Toolbar {
         let has_left_items = self.left_items().count() > 0;
         let has_right_items = self.right_items().count() > 0;
 
-        v_stack()
+        v_flex()
             .p_2()
             .when(has_left_items || has_right_items, |this| this.gap_2())
             .border_b()
             .border_color(cx.theme().colors().border_variant)
             .bg(cx.theme().colors().toolbar_background)
             .child(
-                h_stack()
+                h_flex()
                     .justify_between()
                     .when(has_left_items, |this| {
                         this.child(
-                            h_stack()
+                            h_flex()
                                 .flex_1()
                                 .justify_start()
                                 .children(self.left_items().map(|item| item.to_any())),
@@ -122,7 +122,7 @@ impl Render for Toolbar {
                     })
                     .when(has_right_items, |this| {
                         this.child(
-                            h_stack()
+                            h_flex()
                                 .flex_1()
                                 .justify_end()
                                 .children(self.right_items().map(|item| item.to_any())),

crates/workspace/src/workspace.rs 🔗

@@ -1271,7 +1271,9 @@ impl Workspace {
     }
 
     pub fn open(&mut self, _: &Open, cx: &mut ViewContext<Self>) {
-        self.client().telemetry().report_app_event("open project");
+        self.client()
+            .telemetry()
+            .report_app_event("open project".to_string());
         let paths = cx.prompt_for_paths(PathPromptOptions {
             files: true,
             directories: true,
@@ -1776,6 +1778,12 @@ impl Workspace {
     }
 
     pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
+        if let Some(text) = item.telemetry_event_text(cx) {
+            self.client()
+                .telemetry()
+                .report_app_event(format!("{}: open", text));
+        }
+
         self.active_pane
             .update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
     }
@@ -2250,17 +2258,16 @@ impl Workspace {
         destination_index: usize,
         cx: &mut ViewContext<Self>,
     ) {
-        let item_to_move = source
+        let Some((item_ix, item_handle)) = source
             .read(cx)
             .items()
             .enumerate()
-            .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move);
-
-        if item_to_move.is_none() {
-            log::warn!("Tried to move item handle which was not in `from` pane. Maybe tab was closed during drop");
+            .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
+        else {
+            // Tab was closed during drag
             return;
-        }
-        let (item_ix, item_handle) = item_to_move.unwrap();
+        };
+
         let item_handle = item_handle.clone();
 
         if source != destination {
@@ -3324,36 +3331,6 @@ impl Workspace {
         workspace
     }
 
-    //     fn render_dock(&self, position: DockPosition, cx: &WindowContext) -> Option<AnyElement<Self>> {
-    //         let dock = match position {
-    //             DockPosition::Left => &self.left_dock,
-    //             DockPosition::Right => &self.right_dock,
-    //             DockPosition::Bottom => &self.bottom_dock,
-    //         };
-    //         let active_panel = dock.read(cx).visible_panel()?;
-    //         let element = if Some(active_panel.id()) == self.zoomed.as_ref().map(|zoomed| zoomed.id()) {
-    //             dock.read(cx).render_placeholder(cx)
-    //         } else {
-    //             ChildView::new(dock, cx).into_any()
-    //         };
-
-    //         Some(
-    //             element
-    //                 .constrained()
-    //                 .dynamically(move |constraint, _, cx| match position {
-    //                     DockPosition::Left | DockPosition::Right => SizeConstraint::new(
-    //                         Vector2F::new(20., constraint.min.y()),
-    //                         Vector2F::new(cx.window_size().x() * 0.8, constraint.max.y()),
-    //                     ),
-    //                     DockPosition::Bottom => SizeConstraint::new(
-    //                         Vector2F::new(constraint.min.x(), 20.),
-    //                         Vector2F::new(constraint.max.x(), cx.window_size().y() * 0.8),
-    //                     ),
-    //                 })
-    //                 .into_any(),
-    //         )
-    //     }
-    // }
     pub fn register_action<A: Action>(
         &mut self,
         callback: impl Fn(&mut Self, &A, &mut ViewContext<Self>) + 'static,

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

@@ -184,7 +184,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_python_autoindent(cx: &mut TestAppContext) {
-        // cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
+        cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
         let language =
             crate::languages::language("python", tree_sitter_python::language(), None).await;
         cx.update(|cx| {

crates/zed/src/main.rs 🔗

@@ -176,10 +176,13 @@ fn main() {
         telemetry.start(installation_id, session_id, cx);
         telemetry.report_setting_event("theme", cx.theme().name.to_string());
         telemetry.report_setting_event("keymap", BaseKeymap::get_global(cx).to_string());
-        telemetry.report_app_event(match existing_installation_id_found {
-            Some(false) => "first open",
-            _ => "open",
-        });
+        telemetry.report_app_event(
+            match existing_installation_id_found {
+                Some(false) => "first open",
+                _ => "open",
+            }
+            .to_string(),
+        );
         telemetry.flush_events();
 
         let app_state = Arc::new(AppState {

crates/zed/src/zed.rs 🔗

@@ -113,12 +113,6 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
         })
         .detach();
 
-        // cx.emit(workspace::Event::PaneAdded(workspace.active_pane().clone()));
-
-        //     let collab_titlebar_item =
-        //         cx.add_view(|cx| CollabTitlebarItem::new(workspace, &workspace_handle, cx));
-        //     workspace.set_titlebar_item(collab_titlebar_item.into_any(), cx);
-
         let copilot = cx.new_view(|cx| copilot_ui::CopilotButton::new(app_state.fs.clone(), cx));
         let diagnostic_summary =
             cx.new_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
@@ -184,7 +178,10 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
             )?;
 
             workspace_handle.update(&mut cx, |workspace, cx| {
-                let position = project_panel.read(cx).position(cx);
+                let (position, was_deserialized) = {
+                    let project_panel = project_panel.read(cx);
+                    (project_panel.position(cx), project_panel.was_deserialized())
+                };
                 workspace.add_panel(project_panel, cx);
                 workspace.add_panel(terminal_panel, cx);
                 workspace.add_panel(assistant_panel, cx);
@@ -192,15 +189,16 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
                 workspace.add_panel(chat_panel, cx);
                 workspace.add_panel(notification_panel, cx);
 
-                if workspace
-                    .project()
-                    .read(cx)
-                    .visible_worktrees(cx)
-                    .any(|tree| {
-                        tree.read(cx)
-                            .root_entry()
-                            .map_or(false, |entry| entry.is_dir())
-                    })
+                if !was_deserialized
+                    && workspace
+                        .project()
+                        .read(cx)
+                        .visible_worktrees(cx)
+                        .any(|tree| {
+                            tree.read(cx)
+                                .root_entry()
+                                .map_or(false, |entry| entry.is_dir())
+                        })
                 {
                     workspace.toggle_dock(position, cx);
                 }

script/squawk 🔗

@@ -0,0 +1,32 @@
+#!/usr/bin/env bash
+
+# Squawk is a linter for database migrations. It helps identify dangerous patterns, and suggests alternatives.
+# Squawk flagging an error does not mean that you need to take a different approach, but it does indicate you need to think about what you're doing.
+# See also: https://squawkhq.com
+
+set -e
+
+if [ -z "$GITHUB_BASE_REF" ]; then
+  echo 'Not a pull request, skipping squawk modified migrations linting'
+  return 0
+fi
+
+SQUAWK_VERSION=0.26.0
+SQUAWK_BIN="./target/squawk-$SQUAWK_VERSION"
+SQUAWK_ARGS="--assume-in-transaction"
+
+
+if  [ ! -f "$SQUAWK_BIN" ]; then
+  curl -L -o "$SQUAWK_BIN" "https://github.com/sbdchd/squawk/releases/download/v$SQUAWK_VERSION/squawk-darwin-x86_64"
+  chmod +x "$SQUAWK_BIN"
+fi
+
+if [ -n "$SQUAWK_GITHUB_TOKEN" ]; then
+    export SQUAWK_GITHUB_REPO_OWNER=$(echo $GITHUB_REPOSITORY | awk -F/ '{print $1}')
+    export SQUAWK_GITHUB_REPO_NAME=$(echo $GITHUB_REPOSITORY | awk -F/ '{print $2}')
+    export SQUAWK_GITHUB_PR_NUMBER=$(echo $GITHUB_REF | awk 'BEGIN { FS = "/" } ; { print $3 }')
+
+    $SQUAWK_BIN $SQUAWK_ARGS upload-to-github $(git diff --name-only origin/$GITHUB_BASE_REF...origin/$GITHUB_HEAD_REF 'crates/collab/migrations/*.sql')
+else
+    $SQUAWK_BIN $SQUAWK_ARGS $(git ls-files --others crates/collab/migrations/*.sql) $(git diff --name-only main crates/collab/migrations/*.sql)
+fi

script/zed-local 🔗

@@ -1,31 +1,44 @@
 #!/usr/bin/env node
 
+const HELP = `
+USAGE
+  zed-local  [options]  [zed args]
+
+OPTIONS
+  --help        Print this help message
+  --release     Build Zed in release mode
+  -2, -3, -4    Spawn 2, 3, or 4 Zed instances, with their windows tiled.
+  --top         Arrange the Zed windows so they take up the top half of the screen.
+`.trim();
+
 const { spawn, execFileSync } = require("child_process");
 
 const RESOLUTION_REGEX = /(\d+) x (\d+)/;
 const DIGIT_FLAG_REGEX = /^--?(\d+)$/;
 
-// Parse the number of Zed instances to spawn.
 let instanceCount = 1;
 let isReleaseMode = false;
 let isTop = false;
 
 const args = process.argv.slice(2);
-for (const arg of args) {
+while (args.length > 0) {
+  const arg = args[0];
+
   const digitMatch = arg.match(DIGIT_FLAG_REGEX);
   if (digitMatch) {
     instanceCount = parseInt(digitMatch[1]);
-    continue;
-  }
-
-  if (arg == "--release") {
+  } else if (arg === "--release") {
     isReleaseMode = true;
-    continue;
-  }
-
-  if (arg == "--top") {
+  } else if (arg === "--top") {
     isTop = true;
+  } else if (arg === "--help") {
+    console.log(HELP);
+    process.exit(0);
+  } else {
+    break;
   }
+
+  args.shift();
 }
 
 // Parse the resolution of the main screen