Merge branch 'main' into v0.119.x

Joseph T. Lyons created

Change summary

Cargo.lock                                                       |  78 
assets/keymaps/default.json                                      |   2 
assets/keymaps/vim.json                                          |   8 
crates/client/src/telemetry.rs                                   |  20 
crates/collab_ui/src/chat_panel.rs                               |   9 
crates/collab_ui/src/collab_panel.rs                             | 284 
crates/collab_ui/src/collab_panel/channel_modal.rs               |  10 
crates/collab_ui/src/collab_titlebar_item.rs                     |  93 
crates/collab_ui/src/collab_ui.rs                                |   5 
crates/color/Cargo.toml                                          |  32 
crates/color/src/color.rs                                        | 234 +
crates/editor/src/element.rs                                     |  14 
crates/editor/src/hover_popover.rs                               |   1 
crates/feature_flags/src/feature_flags.rs                        |   6 
crates/gpui/src/platform.rs                                      |   2 
crates/gpui/src/platform/mac/metal_renderer.rs                   |  31 
crates/gpui/src/platform/mac/text_system.rs                      |  18 
crates/gpui/src/scene.rs                                         |  20 
crates/gpui/src/style.rs                                         |   6 
crates/gpui/src/text_system.rs                                   |   5 
crates/gpui/src/window.rs                                        |   8 
crates/gpui_macros/src/style_helpers.rs                          |  14 
crates/search/src/project_search.rs                              | 143 
crates/terminal/Cargo.toml                                       |   1 
crates/terminal/src/terminal_settings.rs                         |  41 
crates/terminal_view/src/terminal_element.rs                     | 109 
crates/theme/Cargo.toml                                          |   1 
crates/theme/src/settings.rs                                     |   6 
crates/ui/src/components/avatar.rs                               | 140 
crates/ui/src/components/avatar/avatar.rs                        | 130 
crates/ui/src/components/avatar/avatar_audio_status_indicator.rs |  65 
crates/ui/src/components/avatar/avatar_availability_indicator.rs |  48 
crates/ui/src/components/button/icon_button.rs                   |  17 
crates/ui/src/components/context_menu.rs                         |   1 
crates/ui/src/components/icon.rs                                 |   2 
crates/ui/src/components/stories/avatar.rs                       |  68 
crates/ui/src/components/stories/icon_button.rs                  |  31 
crates/vim/src/test.rs                                           |  24 
crates/welcome/src/welcome.rs                                    | 361 +
crates/workspace/src/modal_layer.rs                              |  26 
crates/workspace/src/pane.rs                                     |   1 
crates/workspace/src/pane_group.rs                               |  75 
crates/workspace/src/workspace.rs                                |   9 
crates/zed/Cargo.toml                                            |   1 
44 files changed, 1,439 insertions(+), 761 deletions(-)

Detailed changes

Cargo.lock ๐Ÿ”—

@@ -1580,6 +1580,28 @@ dependencies = [
  "rustc-hash",
 ]
 
+[[package]]
+name = "color"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "fs",
+ "indexmap 1.9.3",
+ "itertools 0.11.0",
+ "palette",
+ "parking_lot 0.11.2",
+ "refineable",
+ "schemars",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "settings",
+ "story",
+ "toml 0.5.11",
+ "util",
+ "uuid 1.4.1",
+]
+
 [[package]]
 name = "color_quant"
 version = "1.1.0"
@@ -4977,6 +4999,7 @@ dependencies = [
  "approx",
  "fast-srgb8",
  "palette_derive",
+ "phf",
 ]
 
 [[package]]
@@ -5165,6 +5188,48 @@ dependencies = [
  "indexmap 2.0.0",
 ]
 
+[[package]]
+name = "phf"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
+dependencies = [
+ "phf_macros",
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0"
+dependencies = [
+ "phf_shared",
+ "rand 0.8.5",
+]
+
+[[package]]
+name = "phf_macros"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.37",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b"
+dependencies = [
+ "siphasher 0.3.11",
+]
+
 [[package]]
 name = "picker"
 version = "0.1.0"
@@ -7074,6 +7139,12 @@ version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac"
 
+[[package]]
+name = "siphasher"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
+
 [[package]]
 name = "slab"
 version = "0.4.9"
@@ -7644,7 +7715,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9c536faaff1a10837cfe373142583f6e27d81e96beba339147e77b67c9f260ff"
 dependencies = [
  "float-cmp",
- "siphasher",
+ "siphasher 0.2.3",
 ]
 
 [[package]]
@@ -7775,6 +7846,7 @@ dependencies = [
  "schemars",
  "serde",
  "serde_derive",
+ "serde_json",
  "settings",
  "shellexpand",
  "smallvec",
@@ -7854,6 +7926,7 @@ name = "theme"
 version = "0.1.0"
 dependencies = [
  "anyhow",
+ "color",
  "fs",
  "gpui",
  "indexmap 1.9.3",
@@ -8857,7 +8930,7 @@ dependencies = [
  "roxmltree",
  "rustybuzz",
  "simplecss",
- "siphasher",
+ "siphasher 0.2.3",
  "svgtypes",
  "ttf-parser 0.12.3",
  "unicode-bidi",
@@ -9649,6 +9722,7 @@ dependencies = [
  "client",
  "collab_ui",
  "collections",
+ "color",
  "command_palette",
  "copilot",
  "copilot_ui",

assets/keymaps/default.json ๐Ÿ”—

@@ -402,7 +402,7 @@
       "cmd-r": "workspace::ToggleRightDock",
       "cmd-j": "workspace::ToggleBottomDock",
       "alt-cmd-y": "workspace::CloseAllDocks",
-      "cmd-shift-f": "workspace::DeploySearch",
+      "cmd-shift-f": "pane::DeploySearch",
       "cmd-k cmd-t": "theme_selector::Toggle",
       "cmd-k cmd-s": "zed::OpenKeymap",
       "cmd-t": "project_symbols::Toggle",

assets/keymaps/vim.json ๐Ÿ”—

@@ -99,7 +99,7 @@
       "ctrl-i": "pane::GoForward",
       "ctrl-]": "editor::GoToDefinition",
       "escape": ["vim::SwitchMode", "Normal"],
-      "ctrl+[": ["vim::SwitchMode", "Normal"],
+      "ctrl-[": ["vim::SwitchMode", "Normal"],
       "v": "vim::ToggleVisual",
       "shift-v": "vim::ToggleVisualLine",
       "ctrl-v": "vim::ToggleVisualBlock",
@@ -288,7 +288,7 @@
     "context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting",
     "bindings": {
       "escape": "editor::Cancel",
-      "ctrl+[": "editor::Cancel"
+      "ctrl-[": "editor::Cancel"
     }
   },
   {
@@ -441,7 +441,7 @@
       "r": ["vim::PushOperator", "Replace"],
       "ctrl-c": ["vim::SwitchMode", "Normal"],
       "escape": ["vim::SwitchMode", "Normal"],
-      "ctrl+[": ["vim::SwitchMode", "Normal"],
+      "ctrl-[": ["vim::SwitchMode", "Normal"],
       ">": "editor::Indent",
       "<": "editor::Outdent",
       "i": [
@@ -481,7 +481,7 @@
       "tab": "vim::Tab",
       "enter": "vim::Enter",
       "escape": ["vim::SwitchMode", "Normal"],
-      "ctrl+[": ["vim::SwitchMode", "Normal"]
+      "ctrl-[": ["vim::SwitchMode", "Normal"]
     }
   },
   {

crates/client/src/telemetry.rs ๐Ÿ”—

@@ -143,10 +143,10 @@ const MAX_QUEUE_LEN: usize = 5;
 const MAX_QUEUE_LEN: usize = 50;
 
 #[cfg(debug_assertions)]
-const FLUSH_DEBOUNCE_INTERVAL: Duration = Duration::from_secs(1);
+const FLUSH_INTERVAL: Duration = Duration::from_secs(1);
 
 #[cfg(not(debug_assertions))]
-const FLUSH_DEBOUNCE_INTERVAL: Duration = Duration::from_secs(60 * 5);
+const FLUSH_INTERVAL: Duration = Duration::from_secs(60 * 5);
 
 impl Telemetry {
     pub fn new(client: Arc<dyn HttpClient>, cx: &mut AppContext) -> Arc<Self> {
@@ -457,6 +457,15 @@ impl Telemetry {
             return;
         }
 
+        if state.flush_events_task.is_none() {
+            let this = self.clone();
+            let executor = self.executor.clone();
+            state.flush_events_task = Some(self.executor.spawn(async move {
+                executor.timer(FLUSH_INTERVAL).await;
+                this.flush_events();
+            }));
+        }
+
         let signed_in = state.metrics_id.is_some();
         state.events_queue.push(EventWrapper { signed_in, event });
 
@@ -464,13 +473,6 @@ impl Telemetry {
             if state.events_queue.len() >= MAX_QUEUE_LEN {
                 drop(state);
                 self.flush_events();
-            } else {
-                let this = self.clone();
-                let executor = self.executor.clone();
-                state.flush_events_task = Some(self.executor.spawn(async move {
-                    executor.timer(FLUSH_DEBOUNCE_INTERVAL).await;
-                    this.flush_events();
-                }));
             }
         }
     }

crates/collab_ui/src/chat_panel.rs ๐Ÿ”—

@@ -1,4 +1,4 @@
-use crate::{collab_panel, is_channels_feature_enabled, ChatPanelSettings};
+use crate::{collab_panel, ChatPanelSettings};
 use anyhow::Result;
 use call::{room, ActiveCall};
 use channel::{ChannelChat, ChannelChatEvent, ChannelMessageId, ChannelStore};
@@ -630,9 +630,6 @@ impl Panel for ChatPanel {
         self.active = active;
         if active {
             self.acknowledge_last_message(cx);
-            if !is_channels_feature_enabled(cx) {
-                cx.emit(PanelEvent::Close);
-            }
         }
     }
 
@@ -641,10 +638,6 @@ impl Panel for ChatPanel {
     }
 
     fn icon(&self, cx: &WindowContext) -> Option<ui::IconName> {
-        if !is_channels_feature_enabled(cx) {
-            return None;
-        }
-
         Some(ui::IconName::MessageBubbles).filter(|_| ChatPanelSettings::get_global(cx).button)
     }
 

crates/collab_ui/src/collab_panel.rs ๐Ÿ”—

@@ -12,7 +12,6 @@ use client::{Client, Contact, User, UserStore};
 use contact_finder::ContactFinder;
 use db::kvp::KEY_VALUE_STORE;
 use editor::{Editor, EditorElement, EditorStyle};
-use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
 use fuzzy::{match_strings, StringMatchCandidate};
 use gpui::{
     actions, canvas, div, fill, list, overlay, point, prelude::*, px, AnyElement, AppContext,
@@ -31,8 +30,8 @@ use smallvec::SmallVec;
 use std::{mem, sync::Arc};
 use theme::{ActiveTheme, ThemeSettings};
 use ui::{
-    prelude::*, Avatar, Button, Color, ContextMenu, Icon, IconButton, IconName, IconSize, Label,
-    ListHeader, ListItem, Tooltip,
+    prelude::*, Avatar, AvatarAvailabilityIndicator, Button, Color, ContextMenu, Icon, IconButton,
+    IconName, IconSize, Label, ListHeader, ListItem, Tooltip,
 };
 use util::{maybe, ResultExt, TryFutureExt};
 use workspace::{
@@ -265,10 +264,6 @@ impl CollabPanel {
                 }));
             this.subscriptions
                 .push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx)));
-            this.subscriptions
-                .push(cx.observe_flag::<ChannelsAlpha, _>(move |_, this, cx| {
-                    this.update_entries(true, cx)
-                }));
             this.subscriptions.push(cx.subscribe(
                 &this.channel_store,
                 |this, _channel_store, e, cx| match e {
@@ -504,115 +499,118 @@ impl CollabPanel {
 
         let mut request_entries = Vec::new();
 
-        if cx.has_flag::<ChannelsAlpha>() {
-            self.entries.push(ListEntry::Header(Section::Channels));
+        self.entries.push(ListEntry::Header(Section::Channels));
 
-            if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() {
-                self.match_candidates.clear();
-                self.match_candidates
-                    .extend(channel_store.ordered_channels().enumerate().map(
-                        |(ix, (_, channel))| StringMatchCandidate {
+        if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() {
+            self.match_candidates.clear();
+            self.match_candidates
+                .extend(
+                    channel_store
+                        .ordered_channels()
+                        .enumerate()
+                        .map(|(ix, (_, channel))| StringMatchCandidate {
                             id: ix,
                             string: channel.name.clone().into(),
                             char_bag: channel.name.chars().collect(),
-                        },
-                    ));
-                let matches = executor.block(match_strings(
-                    &self.match_candidates,
-                    &query,
-                    true,
-                    usize::MAX,
-                    &Default::default(),
-                    executor.clone(),
-                ));
-                if let Some(state) = &self.channel_editing_state {
-                    if matches!(state, ChannelEditingState::Create { location: None, .. }) {
-                        self.entries.push(ListEntry::ChannelEditor { depth: 0 });
-                    }
+                        }),
+                );
+            let matches = executor.block(match_strings(
+                &self.match_candidates,
+                &query,
+                true,
+                usize::MAX,
+                &Default::default(),
+                executor.clone(),
+            ));
+            if let Some(state) = &self.channel_editing_state {
+                if matches!(state, ChannelEditingState::Create { location: None, .. }) {
+                    self.entries.push(ListEntry::ChannelEditor { depth: 0 });
                 }
-                let mut collapse_depth = None;
-                for mat in matches {
-                    let channel = channel_store.channel_at_index(mat.candidate_id).unwrap();
-                    let depth = channel.parent_path.len();
-
-                    if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) {
+            }
+            let mut collapse_depth = None;
+            for mat in matches {
+                let channel = channel_store.channel_at_index(mat.candidate_id).unwrap();
+                let depth = channel.parent_path.len();
+
+                if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) {
+                    collapse_depth = Some(depth);
+                } else if let Some(collapsed_depth) = collapse_depth {
+                    if depth > collapsed_depth {
+                        continue;
+                    }
+                    if self.is_channel_collapsed(channel.id) {
                         collapse_depth = Some(depth);
-                    } else if let Some(collapsed_depth) = collapse_depth {
-                        if depth > collapsed_depth {
-                            continue;
-                        }
-                        if self.is_channel_collapsed(channel.id) {
-                            collapse_depth = Some(depth);
-                        } else {
-                            collapse_depth = None;
-                        }
+                    } else {
+                        collapse_depth = None;
                     }
+                }
 
-                    let has_children = channel_store
-                        .channel_at_index(mat.candidate_id + 1)
-                        .map_or(false, |next_channel| {
-                            next_channel.parent_path.ends_with(&[channel.id])
-                        });
+                let has_children = channel_store
+                    .channel_at_index(mat.candidate_id + 1)
+                    .map_or(false, |next_channel| {
+                        next_channel.parent_path.ends_with(&[channel.id])
+                    });
 
-                    match &self.channel_editing_state {
-                        Some(ChannelEditingState::Create {
-                            location: parent_id,
-                            ..
-                        }) if *parent_id == Some(channel.id) => {
-                            self.entries.push(ListEntry::Channel {
-                                channel: channel.clone(),
-                                depth,
-                                has_children: false,
-                            });
-                            self.entries
-                                .push(ListEntry::ChannelEditor { depth: depth + 1 });
-                        }
-                        Some(ChannelEditingState::Rename {
-                            location: parent_id,
-                            ..
-                        }) if parent_id == &channel.id => {
-                            self.entries.push(ListEntry::ChannelEditor { depth });
-                        }
-                        _ => {
-                            self.entries.push(ListEntry::Channel {
-                                channel: channel.clone(),
-                                depth,
-                                has_children,
-                            });
-                        }
+                match &self.channel_editing_state {
+                    Some(ChannelEditingState::Create {
+                        location: parent_id,
+                        ..
+                    }) if *parent_id == Some(channel.id) => {
+                        self.entries.push(ListEntry::Channel {
+                            channel: channel.clone(),
+                            depth,
+                            has_children: false,
+                        });
+                        self.entries
+                            .push(ListEntry::ChannelEditor { depth: depth + 1 });
+                    }
+                    Some(ChannelEditingState::Rename {
+                        location: parent_id,
+                        ..
+                    }) if parent_id == &channel.id => {
+                        self.entries.push(ListEntry::ChannelEditor { depth });
+                    }
+                    _ => {
+                        self.entries.push(ListEntry::Channel {
+                            channel: channel.clone(),
+                            depth,
+                            has_children,
+                        });
                     }
                 }
             }
+        }
 
-            let channel_invites = channel_store.channel_invitations();
-            if !channel_invites.is_empty() {
-                self.match_candidates.clear();
-                self.match_candidates
-                    .extend(channel_invites.iter().enumerate().map(|(ix, channel)| {
-                        StringMatchCandidate {
-                            id: ix,
-                            string: channel.name.clone().into(),
-                            char_bag: channel.name.chars().collect(),
-                        }
-                    }));
-                let matches = executor.block(match_strings(
-                    &self.match_candidates,
-                    &query,
-                    true,
-                    usize::MAX,
-                    &Default::default(),
-                    executor.clone(),
-                ));
-                request_entries.extend(matches.iter().map(|mat| {
-                    ListEntry::ChannelInvite(channel_invites[mat.candidate_id].clone())
+        let channel_invites = channel_store.channel_invitations();
+        if !channel_invites.is_empty() {
+            self.match_candidates.clear();
+            self.match_candidates
+                .extend(channel_invites.iter().enumerate().map(|(ix, channel)| {
+                    StringMatchCandidate {
+                        id: ix,
+                        string: channel.name.clone().into(),
+                        char_bag: channel.name.chars().collect(),
+                    }
                 }));
+            let matches = executor.block(match_strings(
+                &self.match_candidates,
+                &query,
+                true,
+                usize::MAX,
+                &Default::default(),
+                executor.clone(),
+            ));
+            request_entries.extend(
+                matches
+                    .iter()
+                    .map(|mat| ListEntry::ChannelInvite(channel_invites[mat.candidate_id].clone())),
+            );
 
-                if !request_entries.is_empty() {
-                    self.entries
-                        .push(ListEntry::Header(Section::ChannelInvites));
-                    if !self.collapsed_sections.contains(&Section::ChannelInvites) {
-                        self.entries.append(&mut request_entries);
-                    }
+            if !request_entries.is_empty() {
+                self.entries
+                    .push(ListEntry::Header(Section::ChannelInvites));
+                if !self.collapsed_sections.contains(&Section::ChannelInvites) {
+                    self.entries.append(&mut request_entries);
                 }
             }
         }
@@ -2000,43 +1998,49 @@ impl CollabPanel {
         let busy = contact.busy || calling;
         let user_id = contact.user.id;
         let github_login = SharedString::from(contact.user.github_login.clone());
-        let item =
-            ListItem::new(github_login.clone())
-                .indent_level(1)
-                .indent_step_size(px(20.))
-                .selected(is_selected)
-                .on_click(cx.listener(move |this, _, cx| this.call(user_id, cx)))
-                .child(
-                    h_flex()
-                        .w_full()
-                        .justify_between()
-                        .child(Label::new(github_login.clone()))
-                        .when(calling, |el| {
-                            el.child(Label::new("Calling").color(Color::Muted))
-                        })
-                        .when(!calling, |el| {
-                            el.child(
-                                IconButton::new("remove_contact", IconName::Close)
-                                    .icon_color(Color::Muted)
-                                    .visible_on_hover("")
-                                    .tooltip(|cx| Tooltip::text("Remove Contact", cx))
-                                    .on_click(cx.listener({
-                                        let github_login = github_login.clone();
-                                        move |this, _, cx| {
-                                            this.remove_contact(user_id, &github_login, cx);
-                                        }
-                                    })),
-                            )
-                        }),
-                )
-                .start_slot(
-                    // todo handle contacts with no avatar
-                    Avatar::new(contact.user.avatar_uri.clone())
-                        .availability_indicator(if online { Some(!busy) } else { None }),
-                )
-                .when(online && !busy, |el| {
-                    el.on_click(cx.listener(move |this, _, cx| this.call(user_id, cx)))
-                });
+        let item = ListItem::new(github_login.clone())
+            .indent_level(1)
+            .indent_step_size(px(20.))
+            .selected(is_selected)
+            .on_click(cx.listener(move |this, _, cx| this.call(user_id, cx)))
+            .child(
+                h_flex()
+                    .w_full()
+                    .justify_between()
+                    .child(Label::new(github_login.clone()))
+                    .when(calling, |el| {
+                        el.child(Label::new("Calling").color(Color::Muted))
+                    })
+                    .when(!calling, |el| {
+                        el.child(
+                            IconButton::new("remove_contact", IconName::Close)
+                                .icon_color(Color::Muted)
+                                .visible_on_hover("")
+                                .tooltip(|cx| Tooltip::text("Remove Contact", cx))
+                                .on_click(cx.listener({
+                                    let github_login = github_login.clone();
+                                    move |this, _, cx| {
+                                        this.remove_contact(user_id, &github_login, cx);
+                                    }
+                                })),
+                        )
+                    }),
+            )
+            .start_slot(
+                // todo handle contacts with no avatar
+                Avatar::new(contact.user.avatar_uri.clone())
+                    .indicator::<AvatarAvailabilityIndicator>(if online {
+                        Some(AvatarAvailabilityIndicator::new(match busy {
+                            true => ui::Availability::Busy,
+                            false => ui::Availability::Free,
+                        }))
+                    } else {
+                        None
+                    }),
+            )
+            .when(online && !busy, |el| {
+                el.on_click(cx.listener(move |this, _, cx| this.call(user_id, cx)))
+            });
 
         div()
             .id(github_login.clone())
@@ -2310,7 +2314,7 @@ impl CollabPanel {
                             .child(
                                 IconButton::new("channel_chat", IconName::MessageBubbles)
                                     .style(ButtonStyle::Filled)
-                                    .size(ButtonSize::Compact)
+                                    .shape(ui::IconButtonShape::Square)
                                     .icon_size(IconSize::Small)
                                     .icon_color(if has_messages_notification {
                                         Color::Default
@@ -2328,7 +2332,7 @@ impl CollabPanel {
                             .child(
                                 IconButton::new("channel_notes", IconName::File)
                                     .style(ButtonStyle::Filled)
-                                    .size(ButtonSize::Compact)
+                                    .shape(ui::IconButtonShape::Square)
                                     .icon_size(IconSize::Small)
                                     .icon_color(if has_notes_notification {
                                         Color::Default

crates/collab_ui/src/collab_panel/channel_modal.rs ๐Ÿ”—

@@ -348,6 +348,10 @@ impl PickerDelegate for ChannelModalDelegate {
 
     fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
         if let Some((selected_user, role)) = self.user_at_index(self.selected_index) {
+            if Some(selected_user.id) == self.user_store.read(cx).current_user().map(|user| user.id)
+            {
+                return;
+            }
             match self.mode {
                 Mode::ManageMembers => {
                     self.show_context_menu(selected_user, role.unwrap_or(ChannelRole::Member), cx)
@@ -383,6 +387,7 @@ impl PickerDelegate for ChannelModalDelegate {
     ) -> Option<Self::ListItem> {
         let (user, role) = self.user_at_index(ix)?;
         let request_status = self.member_status(user.id, cx);
+        let is_me = self.user_store.read(cx).current_user().map(|user| user.id) == Some(user.id);
 
         Some(
             ListItem::new(ix)
@@ -406,7 +411,10 @@ impl PickerDelegate for ChannelModalDelegate {
                                 Some(ChannelRole::Guest) => Some(Label::new("Guest")),
                                 _ => None,
                             })
-                            .child(IconButton::new("ellipsis", IconName::Ellipsis))
+                            .when(!is_me, |el| {
+                                el.child(IconButton::new("ellipsis", IconName::Ellipsis))
+                            })
+                            .when(is_me, |el| el.child(Label::new("You").color(Color::Muted)))
                             .children(
                                 if let (Some((menu, _)), true) = (&self.context_menu, selected) {
                                     Some(

crates/collab_ui/src/collab_titlebar_item.rs ๐Ÿ”—

@@ -1,9 +1,9 @@
 use crate::face_pile::FacePile;
 use auto_update::AutoUpdateStatus;
 use call::{ActiveCall, ParticipantLocation, Room};
-use client::{proto::PeerId, Client, ParticipantIndex, User, UserStore};
+use client::{proto::PeerId, Client, User, UserStore};
 use gpui::{
-    actions, canvas, div, point, px, rems, Action, AnyElement, AppContext, Element, Hsla,
+    actions, canvas, div, point, px, Action, AnyElement, AppContext, Element, Hsla,
     InteractiveElement, IntoElement, Model, ParentElement, Path, Render,
     StatefulInteractiveElement, Styled, Subscription, View, ViewContext, VisualContext, WeakView,
     WindowBounds,
@@ -12,14 +12,14 @@ use project::{Project, RepositoryEntry};
 use recent_projects::RecentProjects;
 use rpc::proto;
 use std::sync::Arc;
-use theme::{ActiveTheme, PlayerColors};
+use theme::ActiveTheme;
 use ui::{
-    h_flex, popover_menu, prelude::*, Avatar, Button, ButtonLike, ButtonStyle, ContextMenu, Icon,
-    IconButton, IconName, TintColor, Tooltip,
+    h_flex, popover_menu, prelude::*, Avatar, AvatarAudioStatusIndicator, Button, ButtonLike,
+    ButtonStyle, ContextMenu, Icon, IconButton, IconName, TintColor, Tooltip,
 };
 use util::ResultExt;
 use vcs_menu::{build_branch_list, BranchList, OpenRecent as ToggleVcsMenu};
-use workspace::{notifications::NotifyResultExt, Workspace};
+use workspace::{notifications::NotifyResultExt, titlebar_height, Workspace};
 
 const MAX_PROJECT_NAME_LENGTH: usize = 40;
 const MAX_BRANCH_NAME_LENGTH: usize = 40;
@@ -62,10 +62,7 @@ impl Render for CollabTitlebarItem {
             .id("titlebar")
             .justify_between()
             .w_full()
-            .h(rems(1.75))
-            // Set a non-scaling min-height here to ensure the titlebar is
-            // always at least the height of the traffic lights.
-            .min_h(px(32.))
+            .h(titlebar_height(cx))
             .map(|this| {
                 if matches!(cx.window_bounds(), WindowBounds::Fullscreen) {
                     this.pl_2()
@@ -97,7 +94,7 @@ impl Render for CollabTitlebarItem {
                                 room.remote_participants().values().collect::<Vec<_>>();
                             remote_participants.sort_by_key(|p| p.participant_index.0);
 
-                            this.children(self.render_collaborator(
+                            let current_user_face_pile = self.render_collaborator(
                                 &current_user,
                                 peer_id,
                                 true,
@@ -107,7 +104,13 @@ impl Render for CollabTitlebarItem {
                                 project_id,
                                 &current_user,
                                 cx,
-                            ))
+                            );
+
+                            this.children(current_user_face_pile.map(|face_pile| {
+                                v_flex()
+                                    .child(face_pile)
+                                    .child(render_color_ribbon(player_colors.local().cursor))
+                            }))
                             .children(
                                 remote_participants.iter().filter_map(|collaborator| {
                                     let is_present = project_id.map_or(false, |project_id| {
@@ -132,8 +135,11 @@ impl Render for CollabTitlebarItem {
                                             .id(("collaborator", collaborator.user.id))
                                             .child(face_pile)
                                             .child(render_color_ribbon(
-                                                collaborator.participant_index,
-                                                player_colors,
+                                                player_colors
+                                                    .color_for_participant(
+                                                        collaborator.participant_index.0,
+                                                    )
+                                                    .cursor,
                                             ))
                                             .cursor_pointer()
                                             .on_click({
@@ -312,8 +318,7 @@ impl Render for CollabTitlebarItem {
     }
 }
 
-fn render_color_ribbon(participant_index: ParticipantIndex, colors: &PlayerColors) -> gpui::Canvas {
-    let color = colors.color_for_participant(participant_index.0).cursor;
+fn render_color_ribbon(color: Hsla) -> gpui::Canvas {
     canvas(move |bounds, cx| {
         let height = bounds.size.height;
         let horizontal_offset = height;
@@ -472,7 +477,9 @@ impl CollabTitlebarItem {
             return None;
         }
 
+        const FACEPILE_LIMIT: usize = 3;
         let followers = project_id.map_or(&[] as &[_], |id| room.followers_for(peer_id, id));
+        let extra_count = followers.len().saturating_sub(FACEPILE_LIMIT);
 
         let pile = FacePile::default()
             .child(
@@ -480,24 +487,48 @@ impl CollabTitlebarItem {
                     .grayscale(!is_present)
                     .border_color(if is_speaking {
                         cx.theme().status().info_border
-                    } else if is_muted {
-                        cx.theme().status().error_border
                     } else {
-                        Hsla::default()
+                        // We draw the border in a transparent color rather to avoid
+                        // the layout shift that would come with adding/removing the border.
+                        gpui::transparent_black()
+                    })
+                    .when(is_muted, |avatar| {
+                        avatar.indicator(
+                            AvatarAudioStatusIndicator::new(ui::AudioStatus::Muted).tooltip({
+                                let github_login = user.github_login.clone();
+                                move |cx| Tooltip::text(format!("{} is muted", github_login), cx)
+                            }),
+                        )
+                    }),
+            )
+            .children(
+                followers
+                    .iter()
+                    .take(FACEPILE_LIMIT)
+                    .filter_map(|follower_peer_id| {
+                        let follower = room
+                            .remote_participants()
+                            .values()
+                            .find_map(|p| (p.peer_id == *follower_peer_id).then_some(&p.user))
+                            .or_else(|| {
+                                (self.client.peer_id() == Some(*follower_peer_id))
+                                    .then_some(current_user)
+                            })?
+                            .clone();
+
+                        Some(Avatar::new(follower.avatar_uri.clone()))
                     }),
             )
-            .children(followers.iter().filter_map(|follower_peer_id| {
-                let follower = room
-                    .remote_participants()
-                    .values()
-                    .find_map(|p| (p.peer_id == *follower_peer_id).then_some(&p.user))
-                    .or_else(|| {
-                        (self.client.peer_id() == Some(*follower_peer_id)).then_some(current_user)
-                    })?
-                    .clone();
-
-                Some(Avatar::new(follower.avatar_uri.clone()))
-            }));
+            .children(if extra_count > 0 {
+                Some(
+                    div()
+                        .ml_1()
+                        .child(Label::new(format!("+{extra_count}")))
+                        .into_any_element(),
+                )
+            } else {
+                None
+            });
 
         Some(pile)
     }

crates/collab_ui/src/collab_ui.rs ๐Ÿ”—

@@ -12,7 +12,6 @@ use std::{rc::Rc, sync::Arc};
 use call::{report_call_event_for_room, ActiveCall};
 pub use collab_panel::CollabPanel;
 pub use collab_titlebar_item::CollabTitlebarItem;
-use feature_flags::{ChannelsAlpha, FeatureFlagAppExt};
 use gpui::{
     actions, point, AppContext, GlobalPixels, Pixels, PlatformDisplay, Size, Task, WindowBounds,
     WindowKind, WindowOptions,
@@ -121,7 +120,3 @@ fn notification_window_options(
         display_id: Some(screen.id()),
     }
 }
-
-fn is_channels_feature_enabled(cx: &gpui::WindowContext<'_>) -> bool {
-    cx.is_staff() || cx.has_flag::<ChannelsAlpha>()
-}

crates/color/Cargo.toml ๐Ÿ”—

@@ -0,0 +1,32 @@
+[package]
+name = "color"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[features]
+default = []
+stories = ["dep:itertools", "dep:story"]
+
+[lib]
+path = "src/color.rs"
+doctest = true
+
+[dependencies]
+# TODO: Clean up dependencies
+anyhow.workspace = true
+fs = { path = "../fs" }
+indexmap = "1.6.2"
+parking_lot.workspace = true
+refineable.workspace = true
+schemars.workspace = true
+serde.workspace = true
+serde_derive.workspace = true
+serde_json.workspace = true
+settings = { path = "../settings" }
+story = { path = "../story", optional = true }
+toml.workspace = true
+uuid.workspace = true
+util = { path = "../util" }
+itertools = { version = "0.11.0", optional = true }
+palette = "0.7.3"

crates/color/src/color.rs ๐Ÿ”—

@@ -0,0 +1,234 @@
+//! # Color
+//!
+//! The `color` crate provides a set utilities for working with colors. It is a wrapper around the [`palette`](https://docs.rs/palette) crate with some additional functionality.
+//!
+//! It is used to create a manipulate colors when building themes.
+//!
+//! === In development note ===
+//!
+//! This crate is meant to sit between gpui and the theme/ui for all the color related stuff.
+//!
+//! It could be folded into gpui, ui or theme potentially but for now we'll continue
+//! to develop it in isolation.
+//!
+//! Once we have a good idea of the needs of the theme system and color in gpui in general I see 3 paths:
+//! 1. Use `palette` (or another color library) directly in gpui and everywhere else, rather than rolling our own color system.
+//! 2. Keep this crate as a thin wrapper around `palette` and use it everywhere except gpui, and convert to gpui's color system when needed.
+//! 3. Build the needed functionality into gpui and keep using it's color system everywhere.
+//!
+//! I'm leaning towards 2 in the short term and 1 in the long term, but we'll need to discuss it more.
+//!
+//! === End development note ===
+use palette::{
+    blend::Blend, convert::FromColorUnclamped, encoding, rgb::Rgb, Clamp, Mix, Srgb, WithAlpha,
+};
+
+/// The types of blend modes supported
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+pub enum BlendMode {
+    /// Multiplies the colors, resulting in a darker color. This mode is useful for creating shadows.
+    Multiply,
+    /// Lightens the color by adding the source and destination colors. It results in a lighter color.
+    Screen,
+    /// Combines Multiply and Screen blend modes. Parts of the image that are lighter than 50% gray are lightened, and parts that are darker are darkened.
+    Overlay,
+    /// Selects the darker of the base or blend color as the resulting color. Useful for darkening images without affecting the overall contrast.
+    Darken,
+    /// Selects the lighter of the base or blend color as the resulting color. Useful for lightening images without affecting the overall contrast.
+    Lighten,
+    /// Brightens the base color to reflect the blend color. The result is a lightened image.
+    Dodge,
+    /// Darkens the base color to reflect the blend color. The result is a darkened image.
+    Burn,
+    /// Similar to Overlay, but with a stronger effect. Hard Light can either multiply or screen colors, depending on the blend color.
+    HardLight,
+    /// A softer version of Hard Light. Soft Light either darkens or lightens colors, depending on the blend color.
+    SoftLight,
+    /// Subtracts the darker of the two constituent colors from the lighter color. Difference mode is useful for creating more vivid colors.
+    Difference,
+    /// Similar to Difference, but with a lower contrast. Exclusion mode produces an effect similar to Difference but with less intensity.
+    Exclusion,
+}
+
+/// Converts a hexadecimal color string to a `palette::Hsla` color.
+///
+/// This function supports the following hex formats:
+/// `#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA`.
+pub fn hex_to_hsla(s: &str) -> Result<RGBAColor, String> {
+    let hex = s.trim_start_matches('#');
+
+    // Expand shorthand formats #RGB and #RGBA to #RRGGBB and #RRGGBBAA
+    let hex = match hex.len() {
+        3 => hex
+            .chars()
+            .map(|c| c.to_string().repeat(2))
+            .collect::<String>(),
+        4 => {
+            let (rgb, alpha) = hex.split_at(3);
+            let rgb = rgb
+                .chars()
+                .map(|c| c.to_string().repeat(2))
+                .collect::<String>();
+            let alpha = alpha.chars().next().unwrap().to_string().repeat(2);
+            format!("{}{}", rgb, alpha)
+        }
+        6 => format!("{}ff", hex), // Add alpha if missing
+        8 => hex.to_string(),      // Already in full format
+        _ => return Err("Invalid hexadecimal string length".to_string()),
+    };
+
+    let hex_val =
+        u32::from_str_radix(&hex, 16).map_err(|_| format!("Invalid hexadecimal string: {}", s))?;
+
+    let r = ((hex_val >> 24) & 0xFF) as f32 / 255.0;
+    let g = ((hex_val >> 16) & 0xFF) as f32 / 255.0;
+    let b = ((hex_val >> 8) & 0xFF) as f32 / 255.0;
+    let a = (hex_val & 0xFF) as f32 / 255.0;
+
+    let color = RGBAColor { r, g, b, a };
+
+    Ok(color)
+}
+
+// These derives implement to and from palette's color types.
+#[derive(FromColorUnclamped, WithAlpha, Debug, Clone)]
+#[palette(skip_derives(Rgb), rgb_standard = "encoding::Srgb")]
+pub struct RGBAColor {
+    r: f32,
+    g: f32,
+    b: f32,
+    // Let Palette know this is our alpha channel.
+    #[palette(alpha)]
+    a: f32,
+}
+
+impl FromColorUnclamped<RGBAColor> for RGBAColor {
+    fn from_color_unclamped(color: RGBAColor) -> RGBAColor {
+        color
+    }
+}
+
+impl<S> FromColorUnclamped<Rgb<S, f32>> for RGBAColor
+where
+    Srgb: FromColorUnclamped<Rgb<S, f32>>,
+{
+    fn from_color_unclamped(color: Rgb<S, f32>) -> RGBAColor {
+        let srgb = Srgb::from_color_unclamped(color);
+        RGBAColor {
+            r: srgb.red,
+            g: srgb.green,
+            b: srgb.blue,
+            a: 1.0,
+        }
+    }
+}
+
+impl<S> FromColorUnclamped<RGBAColor> for Rgb<S, f32>
+where
+    Rgb<S, f32>: FromColorUnclamped<Srgb>,
+{
+    fn from_color_unclamped(color: RGBAColor) -> Self {
+        let srgb = Srgb::new(color.r, color.g, color.b);
+        Self::from_color_unclamped(srgb)
+    }
+}
+
+impl Clamp for RGBAColor {
+    fn clamp(self) -> Self {
+        RGBAColor {
+            r: self.r.min(1.0).max(0.0),
+            g: self.g.min(1.0).max(0.0),
+            b: self.b.min(1.0).max(0.0),
+            a: self.a.min(1.0).max(0.0),
+        }
+    }
+}
+
+impl RGBAColor {
+    /// Creates a new color from the given RGBA values.
+    ///
+    /// This color can be used to convert to any [`palette::Color`] type.
+    pub fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
+        RGBAColor { r, g, b, a }
+    }
+
+    /// Returns a set of states for this color.
+    pub fn states(self, is_light: bool) -> ColorStates {
+        states_for_color(self, is_light)
+    }
+
+    /// Mixes this color with another [`palette::Hsl`] color at the given `mix_ratio`.
+    pub fn mixed(&self, other: RGBAColor, mix_ratio: f32) -> Self {
+        let srgb_self = Srgb::new(self.r, self.g, self.b);
+        let srgb_other = Srgb::new(other.r, other.g, other.b);
+
+        // Directly mix the colors as sRGB values
+        let mixed = srgb_self.mix(srgb_other, mix_ratio);
+        RGBAColor::from_color_unclamped(mixed)
+    }
+
+    pub fn blend(&self, other: RGBAColor, blend_mode: BlendMode) -> Self {
+        let srgb_self = Srgb::new(self.r, self.g, self.b);
+        let srgb_other = Srgb::new(other.r, other.g, other.b);
+
+        let blended = match blend_mode {
+            // replace hsl methods with the respective sRGB methods
+            BlendMode::Multiply => srgb_self.multiply(srgb_other),
+            _ => unimplemented!(),
+        };
+
+        Self {
+            r: blended.red,
+            g: blended.green,
+            b: blended.blue,
+            a: self.a,
+        }
+    }
+}
+
+/// A set of colors for different states of an element.
+#[derive(Debug, Clone)]
+pub struct ColorStates {
+    /// The default color.
+    pub default: RGBAColor,
+    /// The color when the mouse is hovering over the element.
+    pub hover: RGBAColor,
+    /// The color when the mouse button is held down on the element.
+    pub active: RGBAColor,
+    /// The color when the element is focused with the keyboard.
+    pub focused: RGBAColor,
+    /// The color when the element is disabled.
+    pub disabled: RGBAColor,
+}
+
+/// Returns a set of colors for different states of an element.
+///
+/// todo!("This should take a theme and use appropriate colors from it")
+pub fn states_for_color(color: RGBAColor, is_light: bool) -> ColorStates {
+    let adjustment_factor = if is_light { 0.1 } else { -0.1 };
+    let hover_adjustment = 1.0 - adjustment_factor;
+    let active_adjustment = 1.0 - 2.0 * adjustment_factor;
+    let focused_adjustment = 1.0 - 3.0 * adjustment_factor;
+    let disabled_adjustment = 1.0 - 4.0 * adjustment_factor;
+
+    let make_adjustment = |color: RGBAColor, adjustment: f32| -> RGBAColor {
+        // Adjust lightness for each state
+        // Note: Adjustment logic may differ; simplify as needed for sRGB
+        RGBAColor::new(
+            color.r * adjustment,
+            color.g * adjustment,
+            color.b * adjustment,
+            color.a,
+        )
+    };
+
+    let color = color.clamp();
+
+    ColorStates {
+        default: color.clone(),
+        hover: make_adjustment(color.clone(), hover_adjustment),
+        active: make_adjustment(color.clone(), active_adjustment),
+        focused: make_adjustment(color.clone(), focused_adjustment),
+        disabled: make_adjustment(color.clone(), disabled_adjustment),
+    }
+}

crates/editor/src/element.rs ๐Ÿ”—

@@ -2288,17 +2288,18 @@ impl EditorElement {
                                 .map(|p| SharedString::from(p.to_string_lossy().to_string() + "/"));
                         }
 
-                        div()
+                        v_flex()
                             .id(("path header container", block_id))
                             .size_full()
-                            .p_1p5()
+                            .justify_center()
+                            .p(gpui::px(6.))
                             .child(
                                 h_flex()
                                     .id("path header block")
-                                    .py_1p5()
-                                    .pl_3()
-                                    .pr_2()
-                                    .rounded_lg()
+                                    .size_full()
+                                    .pl(gpui::px(12.))
+                                    .pr(gpui::px(8.))
+                                    .rounded_md()
                                     .shadow_md()
                                     .border()
                                     .border_color(cx.theme().colors().border)
@@ -2861,6 +2862,7 @@ impl Element for EditorElement {
             cx.with_text_style(
                 Some(gpui::TextStyleRefinement {
                     font_size: Some(self.style.text.font_size),
+                    line_height: Some(self.style.text.line_height),
                     ..Default::default()
                 }),
                 |cx| {

crates/feature_flags/src/feature_flags.rs ๐Ÿ”—

@@ -16,12 +16,6 @@ pub trait FeatureFlag {
     const NAME: &'static str;
 }
 
-pub enum ChannelsAlpha {}
-
-impl FeatureFlag for ChannelsAlpha {
-    const NAME: &'static str = "channels_alpha";
-}
-
 pub trait FeatureFlagViewExt<V: 'static> {
     fn observe_flag<T: FeatureFlag, F>(&mut self, callback: F) -> Subscription
     where

crates/gpui/src/platform.rs ๐Ÿ”—

@@ -192,7 +192,7 @@ pub trait PlatformDispatcher: Send + Sync {
 
 pub trait PlatformTextSystem: Send + Sync {
     fn add_fonts(&self, fonts: &[Arc<Vec<u8>>]) -> Result<()>;
-    fn all_font_families(&self) -> Vec<String>;
+    fn all_font_names(&self) -> Vec<String>;
     fn font_id(&self, descriptor: &Font) -> Result<FontId>;
     fn font_metrics(&self, font_id: FontId) -> FontMetrics;
     fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>>;

crates/gpui/src/platform/mac/metal_renderer.rs ๐Ÿ”—

@@ -82,7 +82,7 @@ impl MetalRenderer {
         ];
         let unit_vertices = device.new_buffer_with_data(
             unit_vertices.as_ptr() as *const c_void,
-            (unit_vertices.len() * mem::size_of::<u64>()) as u64,
+            mem::size_of_val(&unit_vertices) as u64,
             MTLResourceOptions::StorageModeManaged,
         );
         let instances = device.new_buffer(
@@ -340,7 +340,8 @@ 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>>();
+            let vertices_bytes_len = mem::size_of_val(vertices.as_slice());
+            let next_offset = *offset + vertices_bytes_len;
             if next_offset > INSTANCE_BUFFER_SIZE {
                 return None;
             }
@@ -373,7 +374,6 @@ impl MetalRenderer {
                 &texture_size as *const Size<DevicePixels> as *const _,
             );
 
-            let vertices_bytes_len = mem::size_of::<PathVertex<ScaledPixels>>() * vertices.len();
             let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
             unsafe {
                 ptr::copy_nonoverlapping(
@@ -430,7 +430,7 @@ impl MetalRenderer {
             &viewport_size as *const Size<DevicePixels> as *const _,
         );
 
-        let shadow_bytes_len = std::mem::size_of_val(shadows);
+        let shadow_bytes_len = mem::size_of_val(shadows);
         let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
 
         let next_offset = *offset + shadow_bytes_len;
@@ -491,7 +491,7 @@ impl MetalRenderer {
             &viewport_size as *const Size<DevicePixels> as *const _,
         );
 
-        let quad_bytes_len = std::mem::size_of_val(quads);
+        let quad_bytes_len = mem::size_of_val(quads);
         let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
 
         let next_offset = *offset + quad_bytes_len;
@@ -591,7 +591,7 @@ impl MetalRenderer {
                 command_encoder
                     .set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture));
 
-                let sprite_bytes_len = mem::size_of::<MonochromeSprite>() * sprites.len();
+                let sprite_bytes_len = mem::size_of_val(sprites.as_slice());
                 let next_offset = *offset + sprite_bytes_len;
                 if next_offset > INSTANCE_BUFFER_SIZE {
                     return false;
@@ -656,21 +656,22 @@ impl MetalRenderer {
             &viewport_size as *const Size<DevicePixels> as *const _,
         );
 
-        let quad_bytes_len = std::mem::size_of_val(underlines);
+        let underline_bytes_len = mem::size_of_val(underlines);
         let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
+
+        let next_offset = *offset + underline_bytes_len;
+        if next_offset > INSTANCE_BUFFER_SIZE {
+            return false;
+        }
+
         unsafe {
             ptr::copy_nonoverlapping(
                 underlines.as_ptr() as *const u8,
                 buffer_contents,
-                quad_bytes_len,
+                underline_bytes_len,
             );
         }
 
-        let next_offset = *offset + quad_bytes_len;
-        if next_offset > INSTANCE_BUFFER_SIZE {
-            return false;
-        }
-
         command_encoder.draw_primitives_instanced(
             metal::MTLPrimitiveType::Triangle,
             0,
@@ -727,7 +728,7 @@ impl MetalRenderer {
         );
         command_encoder.set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture));
 
-        let sprite_bytes_len = std::mem::size_of_val(sprites);
+        let sprite_bytes_len = mem::size_of_val(sprites);
         let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
 
         let next_offset = *offset + sprite_bytes_len;
@@ -799,7 +800,7 @@ impl MetalRenderer {
         );
         command_encoder.set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture));
 
-        let sprite_bytes_len = std::mem::size_of_val(sprites);
+        let sprite_bytes_len = mem::size_of_val(sprites);
         let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
 
         let next_offset = *offset + sprite_bytes_len;

crates/gpui/src/platform/mac/text_system.rs ๐Ÿ”—

@@ -5,7 +5,7 @@ use crate::{
 };
 use anyhow::anyhow;
 use cocoa::appkit::{CGFloat, CGPoint};
-use collections::HashMap;
+use collections::{BTreeSet, HashMap};
 use core_foundation::{
     array::CFIndex,
     attributed_string::{CFAttributedStringRef, CFMutableAttributedString},
@@ -78,12 +78,16 @@ impl PlatformTextSystem for MacTextSystem {
         self.0.write().add_fonts(fonts)
     }
 
-    fn all_font_families(&self) -> Vec<String> {
-        self.0
-            .read()
-            .system_source
-            .all_families()
-            .expect("core text should never return an error")
+    fn all_font_names(&self) -> Vec<String> {
+        let collection = core_text::font_collection::create_for_all_families();
+        let Some(descriptors) = collection.get_descriptors() else {
+            return vec![];
+        };
+        let mut names = BTreeSet::new();
+        for descriptor in descriptors.into_iter() {
+            names.insert(descriptor.display_name());
+        }
+        names.into_iter().collect()
     }
 
     fn font_id(&self, font: &Font) -> Result<FontId> {

crates/gpui/src/scene.rs ๐Ÿ”—

@@ -299,8 +299,8 @@ impl<'a> Iterator for BatchIterator<'a> {
 
         let first = orders_and_kinds[0];
         let second = orders_and_kinds[1];
-        let (batch_kind, max_order) = if first.0.is_some() {
-            (first.1, second.0.unwrap_or(u32::MAX))
+        let (batch_kind, max_order_and_kind) = if first.0.is_some() {
+            (first.1, (second.0.unwrap_or(u32::MAX), second.1))
         } else {
             return None;
         };
@@ -312,7 +312,7 @@ impl<'a> Iterator for BatchIterator<'a> {
                 self.shadows_iter.next();
                 while self
                     .shadows_iter
-                    .next_if(|shadow| shadow.order < max_order)
+                    .next_if(|shadow| (shadow.order, batch_kind) < max_order_and_kind)
                     .is_some()
                 {
                     shadows_end += 1;
@@ -328,7 +328,7 @@ impl<'a> Iterator for BatchIterator<'a> {
                 self.quads_iter.next();
                 while self
                     .quads_iter
-                    .next_if(|quad| quad.order < max_order)
+                    .next_if(|quad| (quad.order, batch_kind) < max_order_and_kind)
                     .is_some()
                 {
                     quads_end += 1;
@@ -342,7 +342,7 @@ impl<'a> Iterator for BatchIterator<'a> {
                 self.paths_iter.next();
                 while self
                     .paths_iter
-                    .next_if(|path| path.order < max_order)
+                    .next_if(|path| (path.order, batch_kind) < max_order_and_kind)
                     .is_some()
                 {
                     paths_end += 1;
@@ -356,7 +356,7 @@ impl<'a> Iterator for BatchIterator<'a> {
                 self.underlines_iter.next();
                 while self
                     .underlines_iter
-                    .next_if(|underline| underline.order < max_order)
+                    .next_if(|underline| (underline.order, batch_kind) < max_order_and_kind)
                     .is_some()
                 {
                     underlines_end += 1;
@@ -374,7 +374,8 @@ impl<'a> Iterator for BatchIterator<'a> {
                 while self
                     .monochrome_sprites_iter
                     .next_if(|sprite| {
-                        sprite.order < max_order && sprite.tile.texture_id == texture_id
+                        (sprite.order, batch_kind) < max_order_and_kind
+                            && sprite.tile.texture_id == texture_id
                     })
                     .is_some()
                 {
@@ -394,7 +395,8 @@ impl<'a> Iterator for BatchIterator<'a> {
                 while self
                     .polychrome_sprites_iter
                     .next_if(|sprite| {
-                        sprite.order < max_order && sprite.tile.texture_id == texture_id
+                        (sprite.order, batch_kind) < max_order_and_kind
+                            && sprite.tile.texture_id == texture_id
                     })
                     .is_some()
                 {
@@ -412,7 +414,7 @@ impl<'a> Iterator for BatchIterator<'a> {
                 self.surfaces_iter.next();
                 while self
                     .surfaces_iter
-                    .next_if(|surface| surface.order < max_order)
+                    .next_if(|surface| (surface.order, batch_kind) < max_order_and_kind)
                     .is_some()
                 {
                     surfaces_end += 1;

crates/gpui/src/style.rs ๐Ÿ”—

@@ -386,7 +386,7 @@ impl Style {
 
         let background_color = self.background.as_ref().and_then(Fill::color);
         if background_color.map_or(false, |color| !color.is_transparent()) {
-            cx.with_z_index(1, |cx| {
+            cx.with_z_index(0, |cx| {
                 let mut border_color = background_color.unwrap_or_default();
                 border_color.a = 0.;
                 cx.paint_quad(quad(
@@ -399,12 +399,12 @@ impl Style {
             });
         }
 
-        cx.with_z_index(2, |cx| {
+        cx.with_z_index(0, |cx| {
             continuation(cx);
         });
 
         if self.is_border_visible() {
-            cx.with_z_index(3, |cx| {
+            cx.with_z_index(0, |cx| {
                 let corner_radii = self.corner_radii.to_pixels(bounds.size, rem_size);
                 let border_widths = self.border_widths.to_pixels(rem_size);
                 let max_border_width = border_widths.max();

crates/gpui/src/text_system.rs ๐Ÿ”—

@@ -65,8 +65,8 @@ impl TextSystem {
         }
     }
 
-    pub fn all_font_families(&self) -> Vec<String> {
-        let mut families = self.platform_text_system.all_font_families();
+    pub fn all_font_names(&self) -> Vec<String> {
+        let mut families = self.platform_text_system.all_font_names();
         families.append(
             &mut self
                 .fallback_font_stack
@@ -101,7 +101,6 @@ impl TextSystem {
         if let Ok(font_id) = self.font_id(font) {
             return font_id;
         }
-
         for fallback in &self.fallback_font_stack {
             if let Ok(font_id) = self.font_id(fallback) {
                 return font_id;

crates/gpui/src/window.rs ๐Ÿ”—

@@ -315,6 +315,7 @@ pub(crate) struct Frame {
     pub(crate) depth_map: Vec<(StackingOrder, EntityId, Bounds<Pixels>)>,
     pub(crate) z_index_stack: StackingOrder,
     pub(crate) next_stacking_order_id: u32,
+    next_root_z_index: u8,
     content_mask_stack: Vec<ContentMask<Pixels>>,
     element_offset_stack: Vec<Point<Pixels>>,
     requested_input_handler: Option<RequestedInputHandler>,
@@ -337,6 +338,7 @@ impl Frame {
             depth_map: Vec::new(),
             z_index_stack: StackingOrder::default(),
             next_stacking_order_id: 0,
+            next_root_z_index: 0,
             content_mask_stack: Vec::new(),
             element_offset_stack: Vec::new(),
             requested_input_handler: None,
@@ -354,6 +356,7 @@ impl Frame {
         self.dispatch_tree.clear();
         self.depth_map.clear();
         self.next_stacking_order_id = 0;
+        self.next_root_z_index = 0;
         self.reused_views.clear();
         self.scene.clear();
         self.requested_input_handler.take();
@@ -2450,8 +2453,13 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
         };
         let new_stacking_order_id =
             post_inc(&mut self.window_mut().next_frame.next_stacking_order_id);
+        let new_root_z_index = post_inc(&mut self.window_mut().next_frame.next_root_z_index);
         let old_stacking_order = mem::take(&mut self.window_mut().next_frame.z_index_stack);
         self.window_mut().next_frame.z_index_stack.id = new_stacking_order_id;
+        self.window_mut()
+            .next_frame
+            .z_index_stack
+            .push(new_root_z_index);
         self.window_mut().next_frame.content_mask_stack.push(mask);
         let result = f(self);
         self.window_mut().next_frame.content_mask_stack.pop();

crates/gpui_macros/src/style_helpers.rs ๐Ÿ”—

@@ -85,6 +85,18 @@ fn generate_methods() -> Vec<TokenStream2> {
     }
 
     for (prefix, fields, prefix_doc_string) in border_prefixes() {
+        methods.push(generate_custom_value_setter(
+            // The plain method names (e.g., `border`, `border_t`, `border_r`, etc.) are special-cased
+            // versions of the 1px variants. This better matches Tailwind, but breaks our existing
+            // convention of the suffix-less variant of the method being the one that accepts a custom value
+            //
+            // To work around this, we're assigning a `_width` suffix here.
+            &format!("{prefix}_width"),
+            quote! { AbsoluteLength },
+            &fields,
+            prefix_doc_string,
+        ));
+
         for (suffix, width_tokens, suffix_doc_string) in border_suffixes() {
             methods.push(generate_predefined_setter(
                 prefix,
@@ -141,7 +153,7 @@ fn generate_predefined_setter(
 }
 
 fn generate_custom_value_setter(
-    prefix: &'static str,
+    prefix: &str,
     length_type: TokenStream2,
     fields: &[TokenStream2],
     doc_string: &str,

crates/search/src/project_search.rs ๐Ÿ”—

@@ -54,14 +54,10 @@ actions!(
     [SearchInNew, ToggleFocus, NextField, ToggleFilters]
 );
 
-#[derive(Default)]
-struct ActiveSearches(HashMap<WeakModel<Project>, WeakView<ProjectSearchView>>);
-
 #[derive(Default)]
 struct ActiveSettings(HashMap<WeakModel<Project>, ProjectSearchSettings>);
 
 pub fn init(cx: &mut AppContext) {
-    cx.set_global(ActiveSearches::default());
     cx.set_global(ActiveSettings::default());
     cx.observe_new_views(|workspace: &mut Workspace, _cx| {
         workspace
@@ -282,6 +278,8 @@ impl EventEmitter<ViewEvent> for ProjectSearchView {}
 
 impl Render for ProjectSearchView {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+        const PLEASE_AUTHENTICATE: &str = "API Key Missing: Please set 'OPENAI_API_KEY' in Environment Variables. If you authenticated using the Assistant Panel, please restart Zed to Authenticate.";
+
         if self.has_matches() {
             div()
                 .flex_1()
@@ -303,40 +301,39 @@ impl Render for ProjectSearchView {
             let mut show_minor_text = true;
             let semantic_status = self.semantic_state.as_ref().and_then(|semantic| {
                 let status = semantic.index_status;
-                                match status {
-                                    SemanticIndexStatus::NotAuthenticated => {
-                                        major_text = Label::new("Not Authenticated");
-                                        show_minor_text = false;
-                                        Some(
-                                            "API Key Missing: Please set 'OPENAI_API_KEY' in Environment Variables. If you authenticated using the Assistant Panel, please restart Zed to Authenticate.".to_string())
-                                    }
-                                    SemanticIndexStatus::Indexed => Some("Indexing complete".to_string()),
-                                    SemanticIndexStatus::Indexing {
-                                        remaining_files,
-                                        rate_limit_expiry,
-                                    } => {
-                                        if remaining_files == 0 {
-                                            Some("Indexing...".to_string())
-                                        } else {
-                                            if let Some(rate_limit_expiry) = rate_limit_expiry {
-                                                let remaining_seconds =
-                                                    rate_limit_expiry.duration_since(Instant::now());
-                                                if remaining_seconds > Duration::from_secs(0) {
-                                                    Some(format!(
-                                                        "Remaining files to index (rate limit resets in {}s): {}",
-                                                        remaining_seconds.as_secs(),
-                                                        remaining_files
-                                                    ))
-                                                } else {
-                                                    Some(format!("Remaining files to index: {}", remaining_files))
-                                                }
-                                            } else {
-                                                Some(format!("Remaining files to index: {}", remaining_files))
-                                            }
-                                        }
-                                    }
-                                    SemanticIndexStatus::NotIndexed => None,
+                match status {
+                    SemanticIndexStatus::NotAuthenticated => {
+                        major_text = Label::new("Not Authenticated");
+                        show_minor_text = false;
+                        Some(PLEASE_AUTHENTICATE.to_string())
+                    }
+                    SemanticIndexStatus::Indexed => Some("Indexing complete".to_string()),
+                    SemanticIndexStatus::Indexing {
+                        remaining_files,
+                        rate_limit_expiry,
+                    } => {
+                        if remaining_files == 0 {
+                            Some("Indexing...".to_string())
+                        } else {
+                            if let Some(rate_limit_expiry) = rate_limit_expiry {
+                                let remaining_seconds =
+                                    rate_limit_expiry.duration_since(Instant::now());
+                                if remaining_seconds > Duration::from_secs(0) {
+                                    Some(format!(
+                                        "Remaining files to index (rate limit resets in {}s): {}",
+                                        remaining_seconds.as_secs(),
+                                        remaining_files
+                                    ))
+                                } else {
+                                    Some(format!("Remaining files to index: {}", remaining_files))
                                 }
+                            } else {
+                                Some(format!("Remaining files to index: {}", remaining_files))
+                            }
+                        }
+                    }
+                    SemanticIndexStatus::NotIndexed => None,
+                }
             });
             let major_text = div().justify_center().max_w_96().child(major_text);
 
@@ -947,25 +944,19 @@ impl ProjectSearchView {
         });
     }
 
-    // Re-activate the most recently activated search or the most recent if it has been closed.
+    // Re-activate the most recently activated search in this pane or the most recent if it has been closed.
     // If no search exists in the workspace, create a new one.
     fn deploy_search(
         workspace: &mut Workspace,
         _: &workspace::DeploySearch,
         cx: &mut ViewContext<Workspace>,
     ) {
-        let active_search = cx
-            .global::<ActiveSearches>()
-            .0
-            .get(&workspace.project().downgrade());
-        let existing = active_search
-            .and_then(|active_search| {
-                workspace
-                    .items_of_type::<ProjectSearchView>(cx)
-                    .filter(|search| &search.downgrade() == active_search)
-                    .last()
-            })
-            .or_else(|| workspace.item_of_type::<ProjectSearchView>(cx));
+        let existing = workspace
+            .active_pane()
+            .read(cx)
+            .items()
+            .find_map(|item| item.downcast::<ProjectSearchView>());
+
         Self::existing_or_new_search(workspace, existing, cx)
     }
 
@@ -983,11 +974,6 @@ impl ProjectSearchView {
         existing: Option<View<ProjectSearchView>>,
         cx: &mut ViewContext<Workspace>,
     ) {
-        // Clean up entries for dropped projects
-        cx.update_global(|state: &mut ActiveSearches, _cx| {
-            state.0.retain(|project, _| project.is_upgradable())
-        });
-
         let query = workspace.active_item(cx).and_then(|item| {
             let editor = item.act_as::<Editor>(cx)?;
             let query = editor.query_suggestion(cx);
@@ -1019,6 +1005,7 @@ impl ProjectSearchView {
             workspace.add_item(Box::new(view.clone()), cx);
             view
         };
+
         search.update(cx, |search, cx| {
             if let Some(query) = query {
                 search.set_query(&query, cx);
@@ -3117,6 +3104,7 @@ pub mod tests {
     async fn test_deploy_search_with_multiple_panes(cx: &mut TestAppContext) {
         init_test(cx);
 
+        // Setup 2 panes, both with a file open and one with a project search.
         let fs = FakeFs::new(cx.background_executor.clone());
         fs.insert_tree(
             "/dir",
@@ -3175,6 +3163,8 @@ pub mod tests {
                 }
             })
             .unwrap();
+
+        // Add a project search item to the second pane
         window
             .update(cx, {
                 let search_bar = search_bar.clone();
@@ -3194,6 +3184,8 @@ pub mod tests {
         cx.run_until_parked();
         assert_eq!(cx.update(|cx| second_pane.read(cx).items_len()), 2);
         assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 1);
+
+        // Focus the first pane
         window
             .update(cx, |workspace, cx| {
                 assert_eq!(workspace.active_pane(), &second_pane);
@@ -3212,20 +3204,47 @@ pub mod tests {
                 assert_eq!(second_pane.read(cx).items_len(), 2);
             })
             .unwrap();
+
+        // Deploy a new search
         cx.dispatch_action(window.into(), DeploySearch);
 
-        // We should have same # of items in workspace, the only difference being that
-        // the search we've deployed previously should now be focused.
+        // Both panes should now have a project search in them
         window
             .update(cx, |workspace, cx| {
-                assert_eq!(workspace.active_pane(), &second_pane);
-                second_pane.update(cx, |this, _| {
+                assert_eq!(workspace.active_pane(), &first_pane);
+                first_pane.update(cx, |this, _| {
                     assert_eq!(this.active_item_index(), 1);
                     assert_eq!(this.items_len(), 2);
                 });
-                first_pane.update(cx, |this, cx| {
+                second_pane.update(cx, |this, cx| {
                     assert!(!cx.focus_handle().contains_focused(cx));
-                    assert_eq!(this.items_len(), 1);
+                    assert_eq!(this.items_len(), 2);
+                });
+            })
+            .unwrap();
+
+        // Focus the second pane's non-search item
+        window
+            .update(cx, |_workspace, cx| {
+                second_pane.update(cx, |pane, cx| pane.activate_next_item(true, cx));
+            })
+            .unwrap();
+
+        // Deploy a new search
+        cx.dispatch_action(window.into(), DeploySearch);
+
+        // The project search view should now be focused in the second pane
+        // And the number of items should be unchanged.
+        window
+            .update(cx, |_workspace, cx| {
+                second_pane.update(cx, |pane, _cx| {
+                    assert!(pane
+                        .active_item()
+                        .unwrap()
+                        .downcast::<ProjectSearchView>()
+                        .is_some());
+
+                    assert_eq!(pane.items_len(), 2);
                 });
             })
             .unwrap();
@@ -3235,7 +3254,7 @@ pub mod tests {
         cx.update(|cx| {
             let settings = SettingsStore::test(cx);
             cx.set_global(settings);
-            cx.set_global(ActiveSearches::default());
+
             SemanticIndexSettings::register(cx);
 
             theme::init(theme::LoadThemes::JustBase, cx);

crates/terminal/Cargo.toml ๐Ÿ”—

@@ -33,6 +33,7 @@ thiserror.workspace = true
 lazy_static.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
+serde_json.workspace = true
 
 [dev-dependencies]
 rand.workspace = true

crates/terminal/src/terminal_settings.rs ๐Ÿ”—

@@ -1,6 +1,12 @@
 use gpui::{px, AbsoluteLength, AppContext, FontFeatures, Pixels};
-use schemars::JsonSchema;
+use schemars::{
+    gen::SchemaGenerator,
+    schema::{InstanceType, RootSchema, Schema, SchemaObject},
+    JsonSchema,
+};
 use serde_derive::{Deserialize, Serialize};
+use serde_json::Value;
+use settings::SettingsJsonSchemaParams;
 use std::{collections::HashMap, path::PathBuf};
 
 #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
@@ -153,6 +159,39 @@ impl settings::Settings for TerminalSettings {
     ) -> anyhow::Result<Self> {
         Self::load_via_json_merge(default_value, user_values)
     }
+    fn json_schema(
+        generator: &mut SchemaGenerator,
+        _: &SettingsJsonSchemaParams,
+        cx: &AppContext,
+    ) -> RootSchema {
+        let mut root_schema = generator.root_schema_for::<Self::FileContent>();
+        let available_fonts = cx
+            .text_system()
+            .all_font_names()
+            .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([("FontFamilies".into(), fonts_schema.into())]);
+        root_schema
+            .schema
+            .object
+            .as_mut()
+            .unwrap()
+            .properties
+            .extend([(
+                "font_family".to_owned(),
+                Schema::new_ref("#/definitions/FontFamilies".into()),
+            )]);
+
+        root_schema
+    }
 }
 
 #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)]

crates/terminal_view/src/terminal_element.rs ๐Ÿ”—

@@ -250,8 +250,8 @@ impl TerminalElement {
 
                 //Layout current cell text
                 {
-                    let cell_text = cell.c.to_string();
                     if !is_blank(&cell) {
+                        let cell_text = cell.c.to_string();
                         let cell_style =
                             TerminalElement::cell_style(&cell, fg, theme, text_style, hyperlink);
 
@@ -586,24 +586,6 @@ impl TerminalElement {
         }
     }
 
-    fn register_key_listeners(&self, cx: &mut WindowContext) {
-        cx.on_key_event({
-            let this = self.terminal.clone();
-            move |event: &ModifiersChangedEvent, phase, cx| {
-                if phase != DispatchPhase::Bubble {
-                    return;
-                }
-
-                let handled =
-                    this.update(cx, |term, _| term.try_modifiers_change(&event.modifiers));
-
-                if handled {
-                    cx.refresh();
-                }
-            }
-        });
-    }
-
     fn register_mouse_listeners(
         &mut self,
         origin: Point<Pixels>,
@@ -771,53 +753,68 @@ impl Element for TerminalElement {
 
         self.register_mouse_listeners(origin, layout.mode, bounds, cx);
 
-        let mut interactivity = mem::take(&mut self.interactivity);
-        interactivity.paint(bounds, bounds.size, state, cx, |_, _, cx| {
-            cx.handle_input(&self.focus, terminal_input_handler);
+        self.interactivity
+            .paint(bounds, bounds.size, state, cx, |_, _, cx| {
+                cx.handle_input(&self.focus, terminal_input_handler);
 
-            self.register_key_listeners(cx);
+                cx.on_key_event({
+                    let this = self.terminal.clone();
+                    move |event: &ModifiersChangedEvent, phase, cx| {
+                        if phase != DispatchPhase::Bubble {
+                            return;
+                        }
 
-            for rect in &layout.rects {
-                rect.paint(origin, &layout, cx);
-            }
+                        let handled =
+                            this.update(cx, |term, _| term.try_modifiers_change(&event.modifiers));
 
-            cx.with_z_index(1, |cx| {
-                for (relative_highlighted_range, color) in layout.relative_highlighted_ranges.iter()
-                {
-                    if let Some((start_y, highlighted_range_lines)) =
-                        to_highlighted_range_lines(relative_highlighted_range, &layout, origin)
-                    {
-                        let hr = HighlightedRange {
-                            start_y, //Need to change this
-                            line_height: layout.dimensions.line_height,
-                            lines: highlighted_range_lines,
-                            color: color.clone(),
-                            //Copied from editor. TODO: move to theme or something
-                            corner_radius: 0.15 * layout.dimensions.line_height,
-                        };
-                        hr.paint(bounds, cx);
+                        if handled {
+                            cx.refresh();
+                        }
                     }
-                }
-            });
+                });
 
-            cx.with_z_index(2, |cx| {
-                for cell in &layout.cells {
-                    cell.paint(origin, &layout, bounds, cx);
+                for rect in &layout.rects {
+                    rect.paint(origin, &layout, cx);
                 }
-            });
 
-            if self.cursor_visible {
-                cx.with_z_index(3, |cx| {
-                    if let Some(cursor) = &layout.cursor {
-                        cursor.paint(origin, cx);
+                cx.with_z_index(1, |cx| {
+                    for (relative_highlighted_range, color) in
+                        layout.relative_highlighted_ranges.iter()
+                    {
+                        if let Some((start_y, highlighted_range_lines)) =
+                            to_highlighted_range_lines(relative_highlighted_range, &layout, origin)
+                        {
+                            let hr = HighlightedRange {
+                                start_y, //Need to change this
+                                line_height: layout.dimensions.line_height,
+                                lines: highlighted_range_lines,
+                                color: color.clone(),
+                                //Copied from editor. TODO: move to theme or something
+                                corner_radius: 0.15 * layout.dimensions.line_height,
+                            };
+                            hr.paint(bounds, cx);
+                        }
                     }
                 });
-            }
 
-            if let Some(mut element) = layout.hyperlink_tooltip.take() {
-                element.draw(origin, bounds.size.map(AvailableSpace::Definite), cx)
-            }
-        });
+                cx.with_z_index(2, |cx| {
+                    for cell in &layout.cells {
+                        cell.paint(origin, &layout, bounds, cx);
+                    }
+                });
+
+                if self.cursor_visible {
+                    cx.with_z_index(3, |cx| {
+                        if let Some(cursor) = &layout.cursor {
+                            cursor.paint(origin, cx);
+                        }
+                    });
+                }
+
+                if let Some(mut element) = layout.hyperlink_tooltip.take() {
+                    element.draw(origin, bounds.size.map(AvailableSpace::Definite), cx)
+                }
+            });
     }
 }
 

crates/theme/Cargo.toml ๐Ÿ”—

@@ -34,6 +34,7 @@ story = { path = "../story", optional = true }
 toml.workspace = true
 uuid.workspace = true
 util = { path = "../util" }
+color = {path = "../color"}
 itertools = { version = "0.11.0", optional = true }
 
 [dev-dependencies]

crates/theme/src/settings.rs ๐Ÿ”—

@@ -205,7 +205,7 @@ impl settings::Settings for ThemeSettings {
 
         let available_fonts = cx
             .text_system()
-            .all_font_families()
+            .all_font_names()
             .into_iter()
             .map(Value::String)
             .collect();
@@ -234,6 +234,10 @@ impl settings::Settings for ThemeSettings {
                     "buffer_font_family".to_owned(),
                     Schema::new_ref("#/definitions/FontFamilies".into()),
                 ),
+                (
+                    "ui_font_family".to_owned(),
+                    Schema::new_ref("#/definitions/FontFamilies".into()),
+                ),
             ]);
 
         root_schema

crates/ui/src/components/avatar.rs ๐Ÿ”—

@@ -1,135 +1,7 @@
-use crate::prelude::*;
-use gpui::{img, Hsla, ImageSource, Img, IntoElement, Styled};
+mod avatar;
+mod avatar_audio_status_indicator;
+mod avatar_availability_indicator;
 
-/// The shape of an [`Avatar`].
-#[derive(Debug, Default, PartialEq, Clone)]
-pub enum AvatarShape {
-    /// The avatar is shown in a circle.
-    #[default]
-    Circle,
-    /// The avatar is shown in a rectangle with rounded corners.
-    RoundedRectangle,
-}
-
-/// An element that renders a user avatar with customizable appearance options.
-///
-/// # Examples
-///
-/// ```
-/// use ui::{Avatar, AvatarShape};
-///
-/// Avatar::new("path/to/image.png")
-///     .shape(AvatarShape::Circle)
-///     .grayscale(true)
-///     .border_color(gpui::red());
-/// ```
-#[derive(IntoElement)]
-pub struct Avatar {
-    image: Img,
-    size: Option<Pixels>,
-    border_color: Option<Hsla>,
-    is_available: Option<bool>,
-}
-
-impl RenderOnce for Avatar {
-    fn render(mut self, cx: &mut WindowContext) -> impl IntoElement {
-        if self.image.style().corner_radii.top_left.is_none() {
-            self = self.shape(AvatarShape::Circle);
-        }
-
-        let size = self.size.unwrap_or_else(|| cx.rem_size());
-
-        div()
-            .size(size + px(2.))
-            .map(|mut div| {
-                div.style().corner_radii = self.image.style().corner_radii.clone();
-                div
-            })
-            .when_some(self.border_color, |this, color| {
-                this.border().border_color(color)
-            })
-            .child(
-                self.image
-                    .size(size)
-                    .bg(cx.theme().colors().ghost_element_background),
-            )
-            .children(self.is_available.map(|is_free| {
-                // HACK: non-integer sizes result in oval indicators.
-                let indicator_size = (size * 0.4).round();
-
-                div()
-                    .absolute()
-                    .z_index(1)
-                    .bg(if is_free {
-                        cx.theme().status().created
-                    } else {
-                        cx.theme().status().deleted
-                    })
-                    .size(indicator_size)
-                    .rounded(indicator_size)
-                    .bottom_0()
-                    .right_0()
-            }))
-    }
-}
-
-impl Avatar {
-    pub fn new(src: impl Into<ImageSource>) -> Self {
-        Avatar {
-            image: img(src),
-            is_available: None,
-            border_color: None,
-            size: None,
-        }
-    }
-
-    /// Sets the shape of the avatar image.
-    ///
-    /// This method allows the shape of the avatar to be specified using a [`Shape`].
-    /// It modifies the corner radius of the image to match the specified shape.
-    ///
-    /// # Examples
-    ///
-    /// ```
-    /// use ui::{Avatar, AvatarShape};
-    ///
-    /// Avatar::new("path/to/image.png").shape(AvatarShape::Circle);
-    /// ```
-    pub fn shape(mut self, shape: AvatarShape) -> Self {
-        self.image = match shape {
-            AvatarShape::Circle => self.image.rounded_full(),
-            AvatarShape::RoundedRectangle => self.image.rounded_md(),
-        };
-        self
-    }
-
-    /// Applies a grayscale filter to the avatar image.
-    ///
-    /// # Examples
-    ///
-    /// ```
-    /// use ui::{Avatar, AvatarShape};
-    ///
-    /// let avatar = Avatar::new("path/to/image.png").grayscale(true);
-    /// ```
-    pub fn grayscale(mut self, grayscale: bool) -> Self {
-        self.image = self.image.grayscale(grayscale);
-        self
-    }
-
-    pub fn border_color(mut self, color: impl Into<Hsla>) -> Self {
-        self.border_color = Some(color.into());
-        self
-    }
-
-    pub fn availability_indicator(mut self, is_available: impl Into<Option<bool>>) -> Self {
-        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
-    }
-}
+pub use avatar::*;
+pub use avatar_audio_status_indicator::*;
+pub use avatar_availability_indicator::*;

crates/ui/src/components/avatar/avatar.rs ๐Ÿ”—

@@ -0,0 +1,130 @@
+use crate::prelude::*;
+
+use gpui::{img, AnyElement, Hsla, ImageSource, Img, IntoElement, Styled};
+
+/// The shape of an [`Avatar`].
+#[derive(Debug, Default, PartialEq, Clone)]
+pub enum AvatarShape {
+    /// The avatar is shown in a circle.
+    #[default]
+    Circle,
+    /// The avatar is shown in a rectangle with rounded corners.
+    RoundedRectangle,
+}
+
+/// An element that renders a user avatar with customizable appearance options.
+///
+/// # Examples
+///
+/// ```
+/// use ui::{Avatar, AvatarShape};
+///
+/// Avatar::new("path/to/image.png")
+///     .shape(AvatarShape::Circle)
+///     .grayscale(true)
+///     .border_color(gpui::red());
+/// ```
+#[derive(IntoElement)]
+pub struct Avatar {
+    image: Img,
+    size: Option<Pixels>,
+    border_color: Option<Hsla>,
+    indicator: Option<AnyElement>,
+}
+
+impl Avatar {
+    pub fn new(src: impl Into<ImageSource>) -> Self {
+        Avatar {
+            image: img(src),
+            size: None,
+            border_color: None,
+            indicator: None,
+        }
+    }
+
+    /// Sets the shape of the avatar image.
+    ///
+    /// This method allows the shape of the avatar to be specified using a [`Shape`].
+    /// It modifies the corner radius of the image to match the specified shape.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use ui::{Avatar, AvatarShape};
+    ///
+    /// Avatar::new("path/to/image.png").shape(AvatarShape::Circle);
+    /// ```
+    pub fn shape(mut self, shape: AvatarShape) -> Self {
+        self.image = match shape {
+            AvatarShape::Circle => self.image.rounded_full(),
+            AvatarShape::RoundedRectangle => self.image.rounded_md(),
+        };
+        self
+    }
+
+    /// Applies a grayscale filter to the avatar image.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use ui::{Avatar, AvatarShape};
+    ///
+    /// let avatar = Avatar::new("path/to/image.png").grayscale(true);
+    /// ```
+    pub fn grayscale(mut self, grayscale: bool) -> Self {
+        self.image = self.image.grayscale(grayscale);
+        self
+    }
+
+    pub fn border_color(mut self, color: impl Into<Hsla>) -> Self {
+        self.border_color = Some(color.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
+    }
+
+    pub fn indicator<E: IntoElement>(mut self, indicator: impl Into<Option<E>>) -> Self {
+        self.indicator = indicator.into().map(IntoElement::into_any_element);
+        self
+    }
+}
+
+impl RenderOnce for Avatar {
+    fn render(mut self, cx: &mut WindowContext) -> impl IntoElement {
+        if self.image.style().corner_radii.top_left.is_none() {
+            self = self.shape(AvatarShape::Circle);
+        }
+
+        let border_width = if self.border_color.is_some() {
+            px(2.)
+        } else {
+            px(0.)
+        };
+
+        let image_size = self.size.unwrap_or_else(|| cx.rem_size());
+        let container_size = image_size + border_width * 2.;
+
+        div()
+            .size(container_size)
+            .map(|mut div| {
+                div.style().corner_radii = self.image.style().corner_radii.clone();
+                div
+            })
+            .when_some(self.border_color, |this, color| {
+                this.border_width(border_width).border_color(color)
+            })
+            .child(
+                self.image
+                    .size(image_size)
+                    .bg(cx.theme().colors().ghost_element_background),
+            )
+            .children(
+                self.indicator
+                    .map(|indicator| div().z_index(1).child(indicator)),
+            )
+    }
+}

crates/ui/src/components/avatar/avatar_audio_status_indicator.rs ๐Ÿ”—

@@ -0,0 +1,65 @@
+use gpui::AnyView;
+
+use crate::prelude::*;
+
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
+pub enum AudioStatus {
+    Muted,
+    Deafened,
+}
+
+#[derive(IntoElement)]
+pub struct AvatarAudioStatusIndicator {
+    audio_status: AudioStatus,
+    tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>,
+}
+
+impl AvatarAudioStatusIndicator {
+    pub fn new(audio_status: AudioStatus) -> Self {
+        Self {
+            audio_status,
+            tooltip: None,
+        }
+    }
+
+    pub fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
+        self.tooltip = Some(Box::new(tooltip));
+        self
+    }
+}
+
+impl RenderOnce for AvatarAudioStatusIndicator {
+    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
+        let icon_size = IconSize::Indicator;
+
+        let width_in_px = icon_size.rems() * cx.rem_size();
+        let padding_x = px(4.);
+
+        div()
+            .absolute()
+            .bottom(rems(-3. / 16.))
+            .right(rems(-6. / 16.))
+            .w(width_in_px + padding_x)
+            .h(icon_size.rems())
+            .child(
+                h_flex()
+                    .id("muted-indicator")
+                    .justify_center()
+                    .px(padding_x)
+                    .py(px(2.))
+                    .bg(cx.theme().status().error_background)
+                    .rounded_md()
+                    .child(
+                        Icon::new(match self.audio_status {
+                            AudioStatus::Muted => IconName::MicMute,
+                            AudioStatus::Deafened => IconName::AudioOff,
+                        })
+                        .size(icon_size)
+                        .color(Color::Error),
+                    )
+                    .when_some(self.tooltip, |this, tooltip| {
+                        this.tooltip(move |cx| tooltip(cx))
+                    }),
+            )
+    }
+}

crates/ui/src/components/avatar/avatar_availability_indicator.rs ๐Ÿ”—

@@ -0,0 +1,48 @@
+use crate::prelude::*;
+
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
+pub enum Availability {
+    Free,
+    Busy,
+}
+
+#[derive(IntoElement)]
+pub struct AvatarAvailabilityIndicator {
+    availability: Availability,
+    avatar_size: Option<Pixels>,
+}
+
+impl AvatarAvailabilityIndicator {
+    pub fn new(availability: Availability) -> Self {
+        Self {
+            availability,
+            avatar_size: None,
+        }
+    }
+
+    /// Sets the size of the [`Avatar`] this indicator appears on.
+    pub fn avatar_size(mut self, size: impl Into<Option<Pixels>>) -> Self {
+        self.avatar_size = size.into();
+        self
+    }
+}
+
+impl RenderOnce for AvatarAvailabilityIndicator {
+    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
+        let avatar_size = self.avatar_size.unwrap_or_else(|| cx.rem_size());
+
+        // HACK: non-integer sizes result in oval indicators.
+        let indicator_size = (avatar_size * 0.4).round();
+
+        div()
+            .absolute()
+            .bottom_0()
+            .right_0()
+            .size(indicator_size)
+            .rounded(indicator_size)
+            .bg(match self.availability {
+                Availability::Free => cx.theme().status().created,
+                Availability::Busy => cx.theme().status().deleted,
+            })
+    }
+}

crates/ui/src/components/button/icon_button.rs ๐Ÿ”—

@@ -127,16 +127,25 @@ impl VisibleOnHover for IconButton {
 }
 
 impl RenderOnce for IconButton {
-    fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
+    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
         let is_disabled = self.base.disabled;
         let is_selected = self.base.selected;
         let selected_style = self.base.selected_style;
 
         self.base
             .map(|this| match self.shape {
-                IconButtonShape::Square => this
-                    .width(self.icon_size.rems().into())
-                    .height(self.icon_size.rems().into()),
+                IconButtonShape::Square => {
+                    let icon_size = self.icon_size.rems() * cx.rem_size();
+                    let padding = match self.icon_size {
+                        IconSize::Indicator => px(0.),
+                        IconSize::XSmall => px(0.),
+                        IconSize::Small => px(2.),
+                        IconSize::Medium => px(2.),
+                    };
+
+                    this.width((icon_size + padding * 2.).into())
+                        .height((icon_size + padding * 2.).into())
+                }
                 IconButtonShape::Wide => this,
             })
             .child(

crates/ui/src/components/context_menu.rs ๐Ÿ”—

@@ -51,6 +51,7 @@ impl ContextMenu {
             let _on_blur_subscription = cx.on_blur(&focus_handle, |this: &mut ContextMenu, cx| {
                 this.cancel(&menu::Cancel, cx)
             });
+            cx.refresh();
             f(
                 Self {
                     items: Default::default(),

crates/ui/src/components/icon.rs ๐Ÿ”—

@@ -5,6 +5,7 @@ use crate::prelude::*;
 
 #[derive(Default, PartialEq, Copy, Clone)]
 pub enum IconSize {
+    Indicator,
     XSmall,
     Small,
     #[default]
@@ -14,6 +15,7 @@ pub enum IconSize {
 impl IconSize {
     pub fn rems(self) -> Rems {
         match self {
+            IconSize::Indicator => rems(10. / 16.),
             IconSize::XSmall => rems(12. / 16.),
             IconSize::Small => rems(14. / 16.),
             IconSize::Medium => rems(16. / 16.),

crates/ui/src/components/stories/avatar.rs ๐Ÿ”—

@@ -1,29 +1,63 @@
 use gpui::Render;
-use story::Story;
+use story::{StoryContainer, StoryItem, StorySection};
 
-use crate::prelude::*;
-use crate::Avatar;
+use crate::{prelude::*, AudioStatus, Availability, AvatarAvailabilityIndicator};
+use crate::{Avatar, AvatarAudioStatusIndicator};
 
 pub struct AvatarStory;
 
 impl Render for AvatarStory {
-    fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
-        Story::container()
-            .child(Story::title_for::<Avatar>())
-            .child(Story::label("Default"))
-            .child(Avatar::new(
-                "https://avatars.githubusercontent.com/u/1714999?v=4",
-            ))
-            .child(Avatar::new(
-                "https://avatars.githubusercontent.com/u/326587?v=4",
-            ))
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+        StoryContainer::new("Avatar", "crates/ui/src/components/stories/avatar.rs")
             .child(
-                Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4")
-                    .availability_indicator(true),
+                StorySection::new()
+                    .child(StoryItem::new(
+                        "Default",
+                        Avatar::new("https://avatars.githubusercontent.com/u/1714999?v=4"),
+                    ))
+                    .child(StoryItem::new(
+                        "Default",
+                        Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4"),
+                    )),
             )
             .child(
-                Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4")
-                    .availability_indicator(false),
+                StorySection::new()
+                    .child(StoryItem::new(
+                        "With free availability indicator",
+                        Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4")
+                            .indicator(AvatarAvailabilityIndicator::new(Availability::Free)),
+                    ))
+                    .child(StoryItem::new(
+                        "With busy availability indicator",
+                        Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4")
+                            .indicator(AvatarAvailabilityIndicator::new(Availability::Busy)),
+                    )),
+            )
+            .child(
+                StorySection::new()
+                    .child(StoryItem::new(
+                        "With info border",
+                        Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4")
+                            .border_color(cx.theme().status().info_border),
+                    ))
+                    .child(StoryItem::new(
+                        "With error border",
+                        Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4")
+                            .border_color(cx.theme().status().error_border),
+                    )),
+            )
+            .child(
+                StorySection::new()
+                    .child(StoryItem::new(
+                        "With muted audio indicator",
+                        Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4")
+                            .indicator(AvatarAudioStatusIndicator::new(AudioStatus::Muted)),
+                    ))
+                    .child(StoryItem::new(
+                        "With deafened audio indicator",
+                        Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4")
+                            .indicator(AvatarAudioStatusIndicator::new(AudioStatus::Deafened)),
+                    )),
             )
     }
 }

crates/ui/src/components/stories/icon_button.rs ๐Ÿ”—

@@ -1,7 +1,7 @@
 use gpui::Render;
 use story::{StoryContainer, StoryItem, StorySection};
 
-use crate::{prelude::*, Tooltip};
+use crate::{prelude::*, IconButtonShape, Tooltip};
 use crate::{IconButton, IconName};
 
 pub struct IconButtonStory;
@@ -115,7 +115,34 @@ impl Render for IconButtonStory {
             "Icon Button",
             "crates/ui2/src/components/stories/icon_button.rs",
         )
-        .children(vec![StorySection::new().children(buttons)])
+        .child(StorySection::new().children(buttons))
+        .child(
+            StorySection::new().child(StoryItem::new(
+                "Square",
+                h_flex()
+                    .gap_2()
+                    .child(
+                        IconButton::new("square-medium", IconName::Close)
+                            .shape(IconButtonShape::Square)
+                            .icon_size(IconSize::Medium),
+                    )
+                    .child(
+                        IconButton::new("square-small", IconName::Close)
+                            .shape(IconButtonShape::Square)
+                            .icon_size(IconSize::Small),
+                    )
+                    .child(
+                        IconButton::new("square-xsmall", IconName::Close)
+                            .shape(IconButtonShape::Square)
+                            .icon_size(IconSize::XSmall),
+                    )
+                    .child(
+                        IconButton::new("square-indicator", IconName::Close)
+                            .shape(IconButtonShape::Square)
+                            .icon_size(IconSize::Indicator),
+                    ),
+            )),
+        )
         .into_element()
     }
 }

crates/vim/src/test.rs ๐Ÿ”—

@@ -71,6 +71,30 @@ async fn test_toggle_through_settings(cx: &mut gpui::TestAppContext) {
     assert_eq!(cx.mode(), Mode::Normal);
 }
 
+#[gpui::test]
+async fn test_cancel_selection(cx: &mut gpui::TestAppContext) {
+    let mut cx = VimTestContext::new(cx, true).await;
+
+    cx.set_state(
+        indoc! {"The quick brown fox juห‡mps over the lazy dog"},
+        Mode::Normal,
+    );
+    // jumps
+    cx.simulate_keystrokes(["v", "l", "l"]);
+    cx.assert_editor_state("The quick brown fox juยซmpsห‡ยป over the lazy dog");
+
+    cx.simulate_keystrokes(["escape"]);
+    cx.assert_editor_state("The quick brown fox jumpห‡s over the lazy dog");
+
+    // go back to the same selection state
+    cx.simulate_keystrokes(["v", "h", "h"]);
+    cx.assert_editor_state("The quick brown fox juยซห‡mpsยป over the lazy dog");
+
+    // Ctrl-[ should behave like Esc
+    cx.simulate_keystrokes(["ctrl-["]);
+    cx.assert_editor_state("The quick brown fox juห‡mps over the lazy dog");
+}
+
 #[gpui::test]
 async fn test_buffer_search(cx: &mut gpui::TestAppContext) {
     let mut cx = VimTestContext::new(cx, true).await;

crates/welcome/src/welcome.rs ๐Ÿ”—

@@ -60,190 +60,197 @@ pub struct WelcomePage {
 
 impl Render for WelcomePage {
     fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
-        h_flex().full().track_focus(&self.focus_handle).child(
-            v_flex()
-                .w_96()
-                .gap_4()
-                .mx_auto()
-                .child(
-                    svg()
-                        .path("icons/logo_96.svg")
-                        .text_color(gpui::white())
-                        .w(px(96.))
-                        .h(px(96.))
-                        .mx_auto(),
-                )
-                .child(
-                    h_flex()
-                        .justify_center()
-                        .child(Label::new("Code at the speed of thought")),
-                )
-                .child(
-                    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".to_string());
-                                    this.workspace
-                                        .update(cx, |workspace, cx| {
-                                            theme_selector::toggle(
-                                                workspace,
-                                                &Default::default(),
-                                                cx,
-                                            )
-                                        })
-                                        .ok();
-                                })),
-                        )
-                        .child(
-                            Button::new("choose-keymap", "Choose a keymap")
-                                .full_width()
-                                .on_click(cx.listener(|this, _, cx| {
-                                    this.telemetry.report_app_event(
-                                        "welcome page: change keymap".to_string(),
-                                    );
-                                    this.workspace
-                                        .update(cx, |workspace, cx| {
-                                            base_keymap_picker::toggle(
-                                                workspace,
-                                                &Default::default(),
-                                                cx,
-                                            )
-                                        })
-                                        .ok();
-                                })),
-                        )
-                        .child(
-                            Button::new("install-cli", "Install the CLI")
-                                .full_width()
-                                .on_click(cx.listener(|this, _, cx| {
-                                    this.telemetry
-                                        .report_app_event("welcome page: install cli".to_string());
-                                    cx.app_mut()
-                                        .spawn(
-                                            |cx| async move { install_cli::install_cli(&cx).await },
+        h_flex()
+            .full()
+            .bg(cx.theme().colors().editor_background)
+            .track_focus(&self.focus_handle)
+            .child(
+                v_flex()
+                    .w_96()
+                    .gap_4()
+                    .mx_auto()
+                    .child(
+                        svg()
+                            .path("icons/logo_96.svg")
+                            .text_color(gpui::white())
+                            .w(px(96.))
+                            .h(px(96.))
+                            .mx_auto(),
+                    )
+                    .child(
+                        h_flex()
+                            .justify_center()
+                            .child(Label::new("Code at the speed of thought")),
+                    )
+                    .child(
+                        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".to_string(),
+                                        );
+                                        this.workspace
+                                            .update(cx, |workspace, cx| {
+                                                theme_selector::toggle(
+                                                    workspace,
+                                                    &Default::default(),
+                                                    cx,
+                                                )
+                                            })
+                                            .ok();
+                                    })),
+                            )
+                            .child(
+                                Button::new("choose-keymap", "Choose a keymap")
+                                    .full_width()
+                                    .on_click(cx.listener(|this, _, cx| {
+                                        this.telemetry.report_app_event(
+                                            "welcome page: change keymap".to_string(),
+                                        );
+                                        this.workspace
+                                            .update(cx, |workspace, cx| {
+                                                base_keymap_picker::toggle(
+                                                    workspace,
+                                                    &Default::default(),
+                                                    cx,
+                                                )
+                                            })
+                                            .ok();
+                                    })),
+                            )
+                            .child(
+                                Button::new("install-cli", "Install the CLI")
+                                    .full_width()
+                                    .on_click(cx.listener(|this, _, cx| {
+                                        this.telemetry.report_app_event(
+                                            "welcome page: install cli".to_string(),
+                                        );
+                                        cx.app_mut()
+                                            .spawn(|cx| async move {
+                                                install_cli::install_cli(&cx).await
+                                            })
+                                            .detach_and_log_err(cx);
+                                    })),
+                            ),
+                    )
+                    .child(
+                        v_flex()
+                            .p_3()
+                            .gap_2()
+                            .bg(cx.theme().colors().elevated_surface_background)
+                            .border_1()
+                            .border_color(cx.theme().colors().border)
+                            .rounded_md()
+                            .child(
+                                h_flex()
+                                    .gap_2()
+                                    .child(
+                                        Checkbox::new(
+                                            "enable-vim",
+                                            if VimModeSetting::get_global(cx).0 {
+                                                ui::Selection::Selected
+                                            } else {
+                                                ui::Selection::Unselected
+                                            },
                                         )
-                                        .detach_and_log_err(cx);
-                                })),
-                        ),
-                )
-                .child(
-                    v_flex()
-                        .p_3()
-                        .gap_2()
-                        .bg(cx.theme().colors().elevated_surface_background)
-                        .border_1()
-                        .border_color(cx.theme().colors().border)
-                        .rounded_md()
-                        .child(
-                            h_flex()
-                                .gap_2()
-                                .child(
-                                    Checkbox::new(
-                                        "enable-vim",
-                                        if VimModeSetting::get_global(cx).0 {
-                                            ui::Selection::Selected
-                                        } else {
-                                            ui::Selection::Unselected
-                                        },
+                                        .on_click(
+                                            cx.listener(move |this, selection, cx| {
+                                                this.telemetry.report_app_event(
+                                                    "welcome page: toggle vim".to_string(),
+                                                );
+                                                this.update_settings::<VimModeSetting>(
+                                                    selection,
+                                                    cx,
+                                                    |setting, value| *setting = Some(value),
+                                                );
+                                            }),
+                                        ),
                                     )
-                                    .on_click(cx.listener(
-                                        move |this, selection, cx| {
-                                            this.telemetry.report_app_event(
-                                                "welcome page: toggle vim".to_string(),
-                                            );
-                                            this.update_settings::<VimModeSetting>(
-                                                selection,
-                                                cx,
-                                                |setting, value| *setting = Some(value),
-                                            );
-                                        },
-                                    )),
-                                )
-                                .child(Label::new("Enable vim mode")),
-                        )
-                        .child(
-                            h_flex()
-                                .gap_2()
-                                .child(
-                                    Checkbox::new(
-                                        "enable-telemetry",
-                                        if TelemetrySettings::get_global(cx).metrics {
-                                            ui::Selection::Selected
-                                        } else {
-                                            ui::Selection::Unselected
-                                        },
-                                    )
-                                    .on_click(cx.listener(
-                                        move |this, selection, cx| {
-                                            this.telemetry.report_app_event(
-                                                "welcome page: toggle metric telemetry".to_string(),
-                                            );
-                                            this.update_settings::<TelemetrySettings>(
-                                                selection,
-                                                cx,
-                                                {
-                                                    let telemetry = this.telemetry.clone();
+                                    .child(Label::new("Enable vim mode")),
+                            )
+                            .child(
+                                h_flex()
+                                    .gap_2()
+                                    .child(
+                                        Checkbox::new(
+                                            "enable-telemetry",
+                                            if TelemetrySettings::get_global(cx).metrics {
+                                                ui::Selection::Selected
+                                            } else {
+                                                ui::Selection::Unselected
+                                            },
+                                        )
+                                        .on_click(
+                                            cx.listener(move |this, selection, cx| {
+                                                this.telemetry.report_app_event(
+                                                    "welcome page: toggle metric telemetry"
+                                                        .to_string(),
+                                                );
+                                                this.update_settings::<TelemetrySettings>(
+                                                    selection,
+                                                    cx,
+                                                    {
+                                                        let telemetry = this.telemetry.clone();
 
-                                                    move |settings, value| {
-                                                        settings.metrics = Some(value);
+                                                        move |settings, value| {
+                                                            settings.metrics = Some(value);
 
-                                                        telemetry.report_setting_event(
-                                                            "metric telemetry",
-                                                            value.to_string(),
-                                                        );
-                                                    }
-                                                },
-                                            );
-                                        },
-                                    )),
-                                )
-                                .child(Label::new("Send anonymous usage data")),
-                        )
-                        .child(
-                            h_flex()
-                                .gap_2()
-                                .child(
-                                    Checkbox::new(
-                                        "enable-crash",
-                                        if TelemetrySettings::get_global(cx).diagnostics {
-                                            ui::Selection::Selected
-                                        } else {
-                                            ui::Selection::Unselected
-                                        },
+                                                            telemetry.report_setting_event(
+                                                                "metric telemetry",
+                                                                value.to_string(),
+                                                            );
+                                                        }
+                                                    },
+                                                );
+                                            }),
+                                        ),
                                     )
-                                    .on_click(cx.listener(
-                                        move |this, selection, cx| {
-                                            this.telemetry.report_app_event(
-                                                "welcome page: toggle diagnostic telemetry"
-                                                    .to_string(),
-                                            );
-                                            this.update_settings::<TelemetrySettings>(
-                                                selection,
-                                                cx,
-                                                {
-                                                    let telemetry = this.telemetry.clone();
+                                    .child(Label::new("Send anonymous usage data")),
+                            )
+                            .child(
+                                h_flex()
+                                    .gap_2()
+                                    .child(
+                                        Checkbox::new(
+                                            "enable-crash",
+                                            if TelemetrySettings::get_global(cx).diagnostics {
+                                                ui::Selection::Selected
+                                            } else {
+                                                ui::Selection::Unselected
+                                            },
+                                        )
+                                        .on_click(
+                                            cx.listener(move |this, selection, cx| {
+                                                this.telemetry.report_app_event(
+                                                    "welcome page: toggle diagnostic telemetry"
+                                                        .to_string(),
+                                                );
+                                                this.update_settings::<TelemetrySettings>(
+                                                    selection,
+                                                    cx,
+                                                    {
+                                                        let telemetry = this.telemetry.clone();
 
-                                                    move |settings, value| {
-                                                        settings.diagnostics = Some(value);
+                                                        move |settings, value| {
+                                                            settings.diagnostics = Some(value);
 
-                                                        telemetry.report_setting_event(
-                                                            "diagnostic telemetry",
-                                                            value.to_string(),
-                                                        );
-                                                    }
-                                                },
-                                            );
-                                        },
-                                    )),
-                                )
-                                .child(Label::new("Send crash reports")),
-                        ),
-                ),
-        )
+                                                            telemetry.report_setting_event(
+                                                                "diagnostic telemetry",
+                                                                value.to_string(),
+                                                            );
+                                                        }
+                                                    },
+                                                );
+                                            }),
+                                        ),
+                                    )
+                                    .child(Label::new("Send crash reports")),
+                            ),
+                    ),
+            )
     }
 }
 

crates/workspace/src/modal_layer.rs ๐Ÿ”—

@@ -27,7 +27,7 @@ impl<V: ModalView> ModalViewHandle for View<V> {
 
 pub struct ActiveModal {
     modal: Box<dyn ModalViewHandle>,
-    _subscription: Subscription,
+    _subscriptions: [Subscription; 2],
     previous_focus_handle: Option<FocusHandle>,
     focus_handle: FocusHandle,
 }
@@ -61,13 +61,19 @@ impl ModalLayer {
     where
         V: ModalView,
     {
+        let focus_handle = cx.focus_handle();
         self.active_modal = Some(ActiveModal {
             modal: Box::new(new_modal.clone()),
-            _subscription: cx.subscribe(&new_modal, |this, _, _: &DismissEvent, cx| {
-                this.hide_modal(cx);
-            }),
+            _subscriptions: [
+                cx.subscribe(&new_modal, |this, _, _: &DismissEvent, cx| {
+                    this.hide_modal(cx);
+                }),
+                cx.on_focus_out(&focus_handle, |this, cx| {
+                    this.hide_modal(cx);
+                }),
+            ],
             previous_focus_handle: cx.focused(),
-            focus_handle: cx.focus_handle(),
+            focus_handle,
         });
         cx.focus_view(&new_modal);
         cx.notify();
@@ -108,7 +114,7 @@ impl ModalLayer {
 }
 
 impl Render for ModalLayer {
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+    fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
         let Some(active_modal) = &self.active_modal else {
             return div();
         };
@@ -127,13 +133,7 @@ impl Render for ModalLayer {
                     .flex_col()
                     .items_center()
                     .track_focus(&active_modal.focus_handle)
-                    .child(
-                        h_flex()
-                            .on_mouse_down_out(cx.listener(|this, _, cx| {
-                                this.hide_modal(cx);
-                            }))
-                            .child(active_modal.modal.view()),
-                    ),
+                    .child(h_flex().child(active_modal.modal.view())),
             )
     }
 }

crates/workspace/src/pane_group.rs ๐Ÿ”—

@@ -3,14 +3,14 @@ use anyhow::{anyhow, Result};
 use call::{ActiveCall, ParticipantLocation};
 use collections::HashMap;
 use gpui::{
-    point, size, AnyView, AnyWeakView, Axis, Bounds, Entity as _, IntoElement, Model, Pixels,
+    point, size, AnyView, AnyWeakView, Axis, Bounds, IntoElement, Model, MouseButton, Pixels,
     Point, View, ViewContext,
 };
 use parking_lot::Mutex;
 use project::Project;
 use serde::Deserialize;
 use std::sync::Arc;
-use ui::{prelude::*, Button};
+use ui::prelude::*;
 
 pub const HANDLE_HITBOX_SIZE: f32 = 4.0;
 const HORIZONTAL_MIN_SIZE: f32 = 80.;
@@ -183,6 +183,7 @@ impl Member {
 
                 let mut leader_border = None;
                 let mut leader_status_box = None;
+                let mut leader_join_data = None;
                 if let Some(leader) = &leader {
                     let mut leader_color = cx
                         .theme()
@@ -199,44 +200,21 @@ impl Member {
                             if Some(leader_project_id) == project.read(cx).remote_id() {
                                 None
                             } else {
-                                let leader_user = leader.user.clone();
-                                let leader_user_id = leader.user.id;
-                                Some(
-                                    Button::new(
-                                        ("leader-status", pane.entity_id()),
-                                        format!(
-                                            "Follow {} to their active project",
-                                            leader_user.github_login,
-                                        ),
-                                    )
-                                    .on_click(cx.listener(
-                                        move |this, _, cx| {
-                                            crate::join_remote_project(
-                                                leader_project_id,
-                                                leader_user_id,
-                                                this.app_state().clone(),
-                                                cx,
-                                            )
-                                            .detach_and_log_err(cx);
-                                        },
-                                    )),
-                                )
+                                leader_join_data = Some((leader_project_id, leader.user.id));
+                                Some(Label::new(format!(
+                                    "Follow {} to their active project",
+                                    leader.user.github_login,
+                                )))
                             }
                         }
-                        ParticipantLocation::UnsharedProject => Some(Button::new(
-                            ("leader-status", pane.entity_id()),
-                            format!(
-                                "{} is viewing an unshared Zed project",
-                                leader.user.github_login
-                            ),
-                        )),
-                        ParticipantLocation::External => Some(Button::new(
-                            ("leader-status", pane.entity_id()),
-                            format!(
-                                "{} is viewing a window outside of Zed",
-                                leader.user.github_login
-                            ),
-                        )),
+                        ParticipantLocation::UnsharedProject => Some(Label::new(format!(
+                            "{} is viewing an unshared Zed project",
+                            leader.user.github_login
+                        ))),
+                        ParticipantLocation::External => Some(Label::new(format!(
+                            "{} is viewing a window outside of Zed",
+                            leader.user.github_login
+                        ))),
                     };
                 }
 
@@ -263,8 +241,27 @@ impl Member {
                                 .w_96()
                                 .bottom_3()
                                 .right_3()
+                                .elevation_2(cx)
+                                .p_1()
                                 .z_index(1)
-                                .child(status_box),
+                                .child(status_box)
+                                .when_some(
+                                    leader_join_data,
+                                    |this, (leader_project_id, leader_user_id)| {
+                                        this.cursor_pointer().on_mouse_down(
+                                            MouseButton::Left,
+                                            cx.listener(move |this, _, cx| {
+                                                crate::join_remote_project(
+                                                    leader_project_id,
+                                                    leader_user_id,
+                                                    this.app_state().clone(),
+                                                    cx,
+                                                )
+                                                .detach_and_log_err(cx);
+                                            }),
+                                        )
+                                    },
+                                ),
                         )
                     })
                     .into_any()

crates/workspace/src/workspace.rs ๐Ÿ”—

@@ -25,7 +25,7 @@ use futures::{
     Future, FutureExt, StreamExt,
 };
 use gpui::{
-    actions, canvas, div, impl_actions, point, size, Action, AnyElement, AnyModel, AnyView,
+    actions, canvas, div, impl_actions, point, px, size, Action, AnyElement, AnyModel, AnyView,
     AnyWeakView, AppContext, AsyncAppContext, AsyncWindowContext, BorrowWindow, Bounds, Context,
     Div, DragMoveEvent, Element, Entity, EntityId, EventEmitter, FocusHandle, FocusableView,
     GlobalPixels, InteractiveElement, IntoElement, KeyContext, LayoutId, ManagedView, Model,
@@ -108,7 +108,6 @@ actions!(
         NewCenterTerminal,
         ToggleTerminalFocus,
         NewSearch,
-        DeploySearch,
         Feedback,
         Restart,
         Welcome,
@@ -4303,6 +4302,10 @@ fn parse_pixel_size_env_var(value: &str) -> Option<Size<GlobalPixels>> {
     Some(size((width as f64).into(), (height as f64).into()))
 }
 
+pub fn titlebar_height(cx: &mut WindowContext) -> Pixels {
+    (1.75 * cx.rem_size()).max(px(32.))
+}
+
 struct DisconnectedOverlay;
 
 impl Element for DisconnectedOverlay {
@@ -4319,7 +4322,7 @@ impl Element for DisconnectedOverlay {
             .bg(background)
             .absolute()
             .left_0()
-            .top_0()
+            .top(titlebar_height(cx))
             .size_full()
             .flex()
             .items_center()

crates/zed/Cargo.toml ๐Ÿ”—

@@ -29,6 +29,7 @@ command_palette = { path = "../command_palette" }
 # component_test = { path = "../component_test" }
 client = { path = "../client" }
 # clock = { path = "../clock" }
+color = { path = "../color" }
 copilot = { path = "../copilot" }
 copilot_ui = { path = "../copilot_ui" }
 diagnostics = { path = "../diagnostics" }