Merge branch 'main' into welcome2

Mikayla created

Change summary

Cargo.lock                                           |   14 
Cargo.toml                                           |    1 
assets/icons/arrow_down.svg                          |    3 
assets/icons/arrow_left.svg                          |    4 
assets/icons/arrow_right.svg                         |    4 
assets/icons/arrow_up.svg                            |    3 
assets/icons/command.svg                             |    1 
assets/icons/control.svg                             |    3 
assets/icons/option.svg                              |    3 
assets/icons/return.svg                              |    3 
assets/icons/shift.svg                               |    3 
crates/auto_update2/src/auto_update.rs               |   11 
crates/auto_update2/src/update_notification.rs       |  102 
crates/call2/src/call2.rs                            |    7 
crates/call2/src/room.rs                             |   13 
crates/collab_ui2/src/collab_panel.rs                | 1123 +++++++------
crates/collab_ui2/src/collab_panel/contact_finder.rs |  266 +-
crates/collab_ui2/src/collab_titlebar_item.rs        |   12 
crates/collab_ui2/src/face_pile.rs                   |    4 
crates/command_palette2/src/command_palette.rs       |   50 
crates/editor2/src/editor.rs                         |   22 
crates/editor2/src/element.rs                        |   73 
crates/editor2/src/hover_popover.rs                  |    3 
crates/file_finder2/src/file_finder.rs               |   29 
crates/gpui2/src/elements/div.rs                     |  131 +
crates/gpui2/src/window.rs                           |   35 
crates/node_runtime/src/node_runtime.rs              |   14 
crates/picker2/src/picker2.rs                        |    6 
crates/project_panel2/src/project_panel.rs           |   66 
crates/storybook2/src/stories/focus.rs               |    9 
crates/storybook2/src/stories/kitchen_sink.rs        |    2 
crates/storybook2/src/stories/picker.rs              |   33 
crates/storybook2/src/stories/scroll.rs              |    2 
crates/storybook2/src/stories/text.rs                |    4 
crates/storybook2/src/stories/z_index.rs             |    4 
crates/storybook2/src/story_selector.rs              |    2 
crates/storybook2/src/storybook2.rs                  |    6 
crates/theme2/src/default_colors.rs                  |   24 
crates/theme2/src/one_themes.rs                      |    4 
crates/theme_selector2/Cargo.toml                    |    1 
crates/theme_selector2/src/theme_selector.rs         |   43 
crates/ui2/Cargo.toml                                |    4 
crates/ui2/src/components/avatar.rs                  |    6 
crates/ui2/src/components/checkbox.rs                |    3 
crates/ui2/src/components/context_menu.rs            |    2 
crates/ui2/src/components/divider.rs                 |   13 
crates/ui2/src/components/icon.rs                    |   33 
crates/ui2/src/components/icon_button.rs             |    9 
crates/ui2/src/components/keybinding.rs              |   90 
crates/ui2/src/components/label.rs                   |    8 
crates/ui2/src/components/list.rs                    |  287 +--
crates/ui2/src/components/popover.rs                 |   23 
crates/ui2/src/components/slot.rs                    |    4 
crates/ui2/src/components/stories.rs                 |    2 
crates/ui2/src/components/stories/avatar.rs          |    2 
crates/ui2/src/components/stories/button.rs          |    4 
crates/ui2/src/components/stories/context_menu.rs    |    6 
crates/ui2/src/components/stories/icon.rs            |    2 
crates/ui2/src/components/stories/icon_button.rs     |   35 
crates/ui2/src/components/stories/input.rs           |    2 
crates/ui2/src/components/stories/keybinding.rs      |    2 
crates/ui2/src/components/stories/label.rs           |    2 
crates/ui2/src/components/stories/list_item.rs       |   10 
crates/ui2/src/components/tooltip.rs                 |    6 
crates/ui2/src/prelude.rs                            |    8 
crates/ui2/src/styled_ext.rs                         |    6 
crates/ui2/src/styles/color.rs                       |    2 
crates/ui2/src/ui2.rs                                |    2 
crates/ui2/src/utils/format_distance.rs              |   55 
crates/welcome2/Cargo.toml                           |    1 
crates/welcome2/src/base_keymap_picker.rs            |   24 
crates/workspace2/src/modal_layer.rs                 |    4 
crates/workspace2/src/workspace2.rs                  |   11 
crates/zed/Cargo.toml                                |    1 
crates/zed/src/languages.rs                          |   13 
crates/zed/src/languages/nu.rs                       |   81 +
crates/zed/src/languages/uiua.rs                     |   55 
crates/zed/src/languages/uiua/config.toml            |   10 
crates/zed/src/languages/uiua/highlights.scm         |   50 
crates/zed/src/languages/uiua/indents.scm            |    3 
crates/zed2/Cargo.toml                               |    3 
crates/zed2/src/languages.rs                         |   13 
crates/zed2/src/languages/nu.rs                      |   55 
crates/zed2/src/languages/uiua.rs                    |   55 
crates/zed2/src/languages/uiua/config.toml           |   10 
crates/zed2/src/languages/uiua/highlights.scm        |   50 
crates/zed2/src/languages/uiua/indents.scm           |    3 
crates/zed2/src/main.rs                              |    4 
88 files changed, 1,862 insertions(+), 1,355 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -9487,6 +9487,7 @@ dependencies = [
  "settings2",
  "smol",
  "theme2",
+ "ui2",
  "util",
  "workspace2",
 ]
@@ -10198,6 +10199,15 @@ dependencies = [
  "tree-sitter",
 ]
 
+[[package]]
+name = "tree-sitter-uiua"
+version = "0.3.3"
+source = "git+https://github.com/shnarazk/tree-sitter-uiua?rev=9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2#9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2"
+dependencies = [
+ "cc",
+ "tree-sitter",
+]
+
 [[package]]
 name = "tree-sitter-vue"
 version = "0.0.1"
@@ -11085,6 +11095,7 @@ dependencies = [
  "settings2",
  "theme2",
  "theme_selector2",
+ "ui2",
  "util",
  "workspace2",
 ]
@@ -11660,6 +11671,7 @@ dependencies = [
  "tree-sitter-svelte",
  "tree-sitter-toml",
  "tree-sitter-typescript",
+ "tree-sitter-uiua",
  "tree-sitter-vue",
  "tree-sitter-yaml",
  "unindent",
@@ -11695,6 +11707,7 @@ dependencies = [
  "auto_update2",
  "backtrace",
  "call2",
+ "channel2",
  "chrono",
  "cli",
  "client2",
@@ -11783,6 +11796,7 @@ dependencies = [
  "tree-sitter-svelte",
  "tree-sitter-toml",
  "tree-sitter-typescript",
+ "tree-sitter-uiua",
  "tree-sitter-vue",
  "tree-sitter-yaml",
  "unindent",

Cargo.toml 🔗

@@ -197,6 +197,7 @@ tree-sitter-lua = "0.0.14"
 tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "66e3e9ce9180ae08fc57372061006ef83f0abde7" }
 tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "786689b0562b9799ce53e824cb45a1a2a04dc673"}
 tree-sitter-vue = {git = "https://github.com/zed-industries/tree-sitter-vue", rev = "9b6cb221ccb8d0b956fcb17e9a1efac2feefeb58"}
+tree-sitter-uiua = {git = "https://github.com/shnarazk/tree-sitter-uiua", rev = "9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2"}
 
 [patch.crates-io]
 tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "3b0159d25559b603af566ade3c83d930bf466db1" }

assets/icons/arrow_down.svg 🔗

@@ -0,0 +1,3 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M8.00001 12L3.5 7.50001M8.00001 12L12.5 7.50001M8.00001 12L8.00001 3.00001" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/arrow_left.svg 🔗

@@ -1,3 +1,3 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M3.125 6.99344L6.35938 3.63281M3.125 6.99344L6.35938 10.3672M3.125 6.99344H11" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M3.5 7.50001L8 3M3.5 7.50001L8 12M3.5 7.50001H12.5" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/arrow_right.svg 🔗

@@ -1,3 +1,3 @@
-<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M10.8906 7.00125L7.64062 3.64062M10.8906 7.00125L7.64062 10.375M10.8906 7.00125H3" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M12.5 7.5L8 12M12.5 7.5L8 3M12.5 7.5L3.5 7.5" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
 </svg>

assets/icons/arrow_up.svg 🔗

@@ -0,0 +1,3 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M7.99999 3.00001L12.5 7.50001M7.99999 3.00001L3.49999 7.50001M7.99999 3.00001L7.99999 12" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/command.svg 🔗

@@ -0,0 +1,3 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">

assets/icons/control.svg 🔗

@@ -0,0 +1,3 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M3.5 6.12488L7.64656 1.97853C7.84183 1.78328 8.1584 1.78329 8.35366 1.97854L12.5 6.12488" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
+</svg>

assets/icons/option.svg 🔗

@@ -0,0 +1,3 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M5.35606 1.005H1.62545C1.28002 1.005 1 1.28502 1 1.63044C1 1.97587 1.28002 2.25589 1.62545 2.25589L5.35606 2.25589C5.62311 2.25589 5.8607 2.42545 5.94752 2.67799L9.75029 13.7387C10.0108 14.4963 10.7235 15.005 11.5247 15.005H14.3746C14.72 15.005 15 14.725 15 14.3796C15 14.0341 14.72 13.7541 14.3746 13.7541H11.5247C11.2576 13.7541 11.02 13.5845 10.9332 13.332L7.13046 2.27128C6.86998 1.51366 6.15721 1.005 5.35606 1.005ZM14.3745 1.005H9.75125C9.40582 1.005 9.1258 1.28502 9.1258 1.63044C9.1258 1.97587 9.40582 2.25589 9.75125 2.25589L14.3745 2.25589C14.72 2.25589 15 1.97587 15 1.63044C15 1.28502 14.72 1.005 14.3745 1.005Z" fill="black"/>
+</svg>

assets/icons/return.svg 🔗

@@ -0,0 +1,3 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M8.375 1.63C8.375 1.28482 8.65482 1.005 9 1.005H12.375C13.8247 1.005 15 2.18025 15 3.63V7.625C15 9.07474 13.8247 10.25 12.375 10.25H3.13388L6.07194 13.1881C6.31602 13.4321 6.31602 13.8279 6.07194 14.0719C5.82786 14.316 5.43214 14.316 5.18806 14.0719L1.18306 10.0669C0.938981 9.82286 0.938981 9.42714 1.18306 9.18306L5.18306 5.18306C5.42714 4.93898 5.82286 4.93898 6.06694 5.18306C6.31102 5.42714 6.31102 5.82286 6.06694 6.06694L3.13388 9H12.375C13.1344 9 13.75 8.38439 13.75 7.625V3.63C13.75 2.87061 13.1344 2.255 12.375 2.255H9C8.65482 2.255 8.375 1.97518 8.375 1.63Z" fill="black"/>
+</svg>

assets/icons/shift.svg 🔗

@@ -0,0 +1,3 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M2.46475 7.99652L7.85304 2.15921C7.93223 2.07342 8.06777 2.07341 8.14696 2.15921L13.5352 7.99652C13.7126 8.18869 13.5763 8.5 13.3148 8.5H10.5V13.7C10.5 13.8657 10.3657 14 10.2 14H5.8C5.63431 14 5.5 13.8657 5.5 13.7V8.5H2.6852C2.42367 8.5 2.28737 8.18869 2.46475 7.99652Z" stroke="black" stroke-width="1.25"/>
+</svg>

crates/auto_update2/src/auto_update.rs 🔗

@@ -84,8 +84,8 @@ impl Settings for AutoUpdateSetting {
 pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppContext) {
     AutoUpdateSetting::register(cx);
 
-    cx.observe_new_views(|wokrspace: &mut Workspace, _cx| {
-        wokrspace
+    cx.observe_new_views(|workspace: &mut Workspace, _cx| {
+        workspace
             .register_action(|_, action: &Check, cx| check(action, cx))
             .register_action(|_, _action: &CheckThatAutoUpdaterWorks, cx| {
                 let prompt = cx.prompt(gpui::PromptLevel::Info, "It does!", &["Ok"]);
@@ -94,6 +94,11 @@ pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppCo
                 })
                 .detach();
             });
+
+        // @nate - code to trigger update notification on launch
+        // workspace.show_notification(0, _cx, |cx| {
+        //     cx.build_view(|_| UpdateNotification::new(SemanticVersion::from_str("1.1.1").unwrap()))
+        // });
     })
     .detach();
 
@@ -131,7 +136,7 @@ pub fn check(_: &Check, cx: &mut AppContext) {
     }
 }
 
-fn _view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) {
+pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) {
     if let Some(auto_updater) = AutoUpdater::get(cx) {
         let auto_updater = auto_updater.read(cx);
         let server_url = &auto_updater.server_url;

crates/auto_update2/src/update_notification.rs 🔗

@@ -1,10 +1,12 @@
 use gpui::{
-    div, DismissEvent, Div, EventEmitter, ParentElement, Render, SemanticVersion, ViewContext,
+    div, DismissEvent, Div, EventEmitter, InteractiveElement, ParentElement, Render,
+    SemanticVersion, StatefulInteractiveElement, Styled, ViewContext,
 };
-use menu::Cancel;
+use util::channel::ReleaseChannel;
+use workspace::ui::{h_stack, v_stack, Icon, IconElement, Label, StyledExt};
 
 pub struct UpdateNotification {
-    _version: SemanticVersion,
+    version: SemanticVersion,
 }
 
 impl EventEmitter<DismissEvent> for UpdateNotification {}
@@ -12,77 +14,43 @@ impl EventEmitter<DismissEvent> for UpdateNotification {}
 impl Render for UpdateNotification {
     type Element = Div;
 
-    fn render(&mut self, _cx: &mut gpui::ViewContext<Self>) -> Self::Element {
-        div().child("Updated zed!")
-        // let theme = theme::current(cx).clone();
-        // let theme = &theme.update_notification;
-
-        // let app_name = cx.global::<ReleaseChannel>().display_name();
-
-        // MouseEventHandler::new::<ViewReleaseNotes, _>(0, cx, |state, cx| {
-        //     Flex::column()
-        //         .with_child(
-        //             Flex::row()
-        //                 .with_child(
-        //                     Text::new(
-        //                         format!("Updated to {app_name} {}", self.version),
-        //                         theme.message.text.clone(),
-        //                     )
-        //                     .contained()
-        //                     .with_style(theme.message.container)
-        //                     .aligned()
-        //                     .top()
-        //                     .left()
-        //                     .flex(1., true),
-        //                 )
-        //                 .with_child(
-        //                     MouseEventHandler::new::<Cancel, _>(0, cx, |state, _| {
-        //                         let style = theme.dismiss_button.style_for(state);
-        //                         Svg::new("icons/x.svg")
-        //                             .with_color(style.color)
-        //                             .constrained()
-        //                             .with_width(style.icon_width)
-        //                             .aligned()
-        //                             .contained()
-        //                             .with_style(style.container)
-        //                             .constrained()
-        //                             .with_width(style.button_width)
-        //                             .with_height(style.button_width)
-        //                     })
-        //                     .with_padding(Padding::uniform(5.))
-        //                     .on_click(MouseButton::Left, move |_, this, cx| {
-        //                         this.dismiss(&Default::default(), cx)
-        //                     })
-        //                     .aligned()
-        //                     .constrained()
-        //                     .with_height(cx.font_cache().line_height(theme.message.text.font_size))
-        //                     .aligned()
-        //                     .top()
-        //                     .flex_float(),
-        //                 ),
-        //         )
-        //         .with_child({
-        //             let style = theme.action_message.style_for(state);
-        //             Text::new("View the release notes", style.text.clone())
-        //                 .contained()
-        //                 .with_style(style.container)
-        //         })
-        //         .contained()
-        // })
-        // .with_cursor_style(CursorStyle::PointingHand)
-        // .on_click(MouseButton::Left, |_, _, cx| {
-        //     crate::view_release_notes(&Default::default(), cx)
-        // })
-        // .into_any_named("update notification")
+    fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
+        let app_name = cx.global::<ReleaseChannel>().display_name();
+
+        v_stack()
+            .elevation_3(cx)
+            .p_4()
+            .child(
+                h_stack()
+                    .justify_between()
+                    .child(Label::new(format!(
+                        "Updated to {app_name} {}",
+                        self.version
+                    )))
+                    .child(
+                        div()
+                            .id("cancel")
+                            .child(IconElement::new(Icon::Close))
+                            .cursor_pointer()
+                            .on_click(cx.listener(|this, _, cx| this.dismiss(cx))),
+                    ),
+            )
+            .child(
+                div()
+                    .id("notes")
+                    .child(Label::new("View the release notes"))
+                    .cursor_pointer()
+                    .on_click(|_, cx| crate::view_release_notes(&Default::default(), cx)),
+            )
     }
 }
 
 impl UpdateNotification {
     pub fn new(version: SemanticVersion) -> Self {
-        Self { _version: version }
+        Self { version }
     }
 
-    pub fn _dismiss(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
+    pub fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
         cx.emit(DismissEvent::Dismiss);
     }
 }

crates/call2/src/call2.rs 🔗

@@ -660,9 +660,12 @@ impl CallHandler for Call {
         self.active_call.as_ref().map(|call| {
             call.0.update(cx, |this, cx| {
                 this.room().map(|room| {
-                    room.update(cx, |this, cx| {
-                        this.toggle_mute(cx).log_err();
+                    let room = room.clone();
+                    cx.spawn(|_, mut cx| async move {
+                        room.update(&mut cx, |this, cx| this.toggle_mute(cx))??
+                            .await
                     })
+                    .detach_and_log_err(cx);
                 })
             })
         });

crates/call2/src/room.rs 🔗

@@ -1,4 +1,7 @@
-use crate::participant::{LocalParticipant, ParticipantLocation, RemoteParticipant};
+use crate::{
+    call_settings::CallSettings,
+    participant::{LocalParticipant, ParticipantLocation, RemoteParticipant},
+};
 use anyhow::{anyhow, Result};
 use audio::{Audio, Sound};
 use client::{
@@ -18,6 +21,7 @@ use live_kit_client::{
 };
 use postage::{sink::Sink, stream::Stream, watch};
 use project::Project;
+use settings::Settings as _;
 use std::{future::Future, mem, sync::Arc, time::Duration};
 use util::{post_inc, ResultExt, TryFutureExt};
 
@@ -328,10 +332,8 @@ impl Room {
         }
     }
 
-    pub fn mute_on_join(_cx: &AppContext) -> bool {
-        // todo!() po: This should be uncommented, though then unmuting does not work
-        false
-        //CallSettings::get_global(cx).mute_on_join || client::IMPERSONATE_LOGIN.is_some()
+    pub fn mute_on_join(cx: &AppContext) -> bool {
+        CallSettings::get_global(cx).mute_on_join || client::IMPERSONATE_LOGIN.is_some()
     }
 
     fn from_join_response(
@@ -1265,7 +1267,6 @@ impl Room {
                     .ok_or_else(|| anyhow!("live-kit was not initialized"))?
                     .await
             };
-
             let publication = publish_track.await;
             this.upgrade()
                 .ok_or_else(|| anyhow!("room was dropped"))?

crates/collab_ui2/src/collab_panel.rs 🔗

@@ -1,5 +1,6 @@
+#![allow(unused)]
 // mod channel_modal;
-// mod contact_finder;
+mod contact_finder;
 
 // use crate::{
 //     channel_view::{self, ChannelView},
@@ -15,7 +16,8 @@
 //     proto::{self, PeerId},
 //     Client, Contact, User, UserStore,
 // };
-// use contact_finder::ContactFinder;
+use contact_finder::ContactFinder;
+use rpc::proto;
 // use context_menu::{ContextMenu, ContextMenuItem};
 // use db::kvp::KEY_VALUE_STORE;
 // use drag_and_drop::{DragAndDrop, Draggable};
@@ -155,26 +157,36 @@ actions!(
 
 const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel";
 
-use std::sync::Arc;
+use std::{iter::once, mem, sync::Arc};
 
-use client::{Client, Contact, UserStore};
+use call::ActiveCall;
+use channel::{Channel, ChannelId, ChannelStore};
+use client::{Client, Contact, User, UserStore};
 use db::kvp::KEY_VALUE_STORE;
+use editor::Editor;
+use feature_flags::{ChannelsAlpha, FeatureFlagAppExt};
+use fuzzy::{match_strings, StringMatchCandidate};
 use gpui::{
-    actions, div, serde_json, AppContext, AsyncWindowContext, Div, EventEmitter, FocusHandle,
-    Focusable, FocusableView, InteractiveElement, Model, ParentElement, Render, Styled, View,
-    ViewContext, VisualContext, WeakView,
+    actions, div, img, prelude::*, serde_json, AppContext, AsyncWindowContext, Div, EventEmitter,
+    FocusHandle, Focusable, FocusableView, InteractiveElement, IntoElement, Model, ParentElement,
+    Render, RenderOnce, SharedString, Styled, Subscription, View, ViewContext, VisualContext,
+    WeakView,
 };
 use project::Fs;
 use serde_derive::{Deserialize, Serialize};
 use settings::Settings;
-use ui::{h_stack, Avatar, Label};
-use util::ResultExt;
+use ui::{
+    h_stack, v_stack, Avatar, Button, Color, Icon, IconButton, Label, List, ListHeader, ListItem,
+    Toggle, Tooltip,
+};
+use util::{maybe, ResultExt};
 use workspace::{
     dock::{DockPosition, Panel, PanelEvent},
+    notifications::NotifyResultExt,
     Workspace,
 };
 
-use crate::CollaborationPanelSettings;
+use crate::{face_pile::FacePile, CollaborationPanelSettings};
 
 pub fn init(cx: &mut AppContext) {
     cx.observe_new_views(|workspace: &mut Workspace, _| {
@@ -268,26 +280,26 @@ pub fn init(cx: &mut AppContext) {
     //     );
 }
 
-// #[derive(Debug)]
-// pub enum ChannelEditingState {
-//     Create {
-//         location: Option<ChannelId>,
-//         pending_name: Option<String>,
-//     },
-//     Rename {
-//         location: ChannelId,
-//         pending_name: Option<String>,
-//     },
-// }
+#[derive(Debug)]
+pub enum ChannelEditingState {
+    Create {
+        location: Option<ChannelId>,
+        pending_name: Option<String>,
+    },
+    Rename {
+        location: ChannelId,
+        pending_name: Option<String>,
+    },
+}
 
-// impl ChannelEditingState {
-//     fn pending_name(&self) -> Option<&str> {
-//         match self {
-//             ChannelEditingState::Create { pending_name, .. } => pending_name.as_deref(),
-//             ChannelEditingState::Rename { pending_name, .. } => pending_name.as_deref(),
-//         }
-//     }
-// }
+impl ChannelEditingState {
+    fn pending_name(&self) -> Option<&str> {
+        match self {
+            ChannelEditingState::Create { pending_name, .. } => pending_name.as_deref(),
+            ChannelEditingState::Rename { pending_name, .. } => pending_name.as_deref(),
+        }
+    }
+}
 
 pub struct CollabPanel {
     width: Option<f32>,
@@ -296,22 +308,22 @@ pub struct CollabPanel {
     // channel_clipboard: Option<ChannelMoveClipboard>,
     // pending_serialization: Task<Option<()>>,
     // context_menu: ViewHandle<ContextMenu>,
-    // filter_editor: ViewHandle<Editor>,
+    filter_editor: View<Editor>,
     // channel_name_editor: ViewHandle<Editor>,
-    // channel_editing_state: Option<ChannelEditingState>,
-    // entries: Vec<ListEntry>,
-    // selection: Option<usize>,
+    channel_editing_state: Option<ChannelEditingState>,
+    entries: Vec<ListEntry>,
+    selection: Option<usize>,
+    channel_store: Model<ChannelStore>,
     user_store: Model<UserStore>,
-    _client: Arc<Client>,
-    // channel_store: ModelHandle<ChannelStore>,
+    client: Arc<Client>,
     // project: ModelHandle<Project>,
-    // match_candidates: Vec<StringMatchCandidate>,
+    match_candidates: Vec<StringMatchCandidate>,
     // list_state: ListState<Self>,
-    // subscriptions: Vec<Subscription>,
-    // collapsed_sections: Vec<Section>,
-    // collapsed_channels: Vec<ChannelId>,
+    subscriptions: Vec<Subscription>,
+    collapsed_sections: Vec<Section>,
+    collapsed_channels: Vec<ChannelId>,
     // drag_target_channel: ChannelDragTarget,
-    _workspace: WeakView<Workspace>,
+    workspace: WeakView<Workspace>,
     // context_menu_on_selected: bool,
 }
 
@@ -335,58 +347,58 @@ struct SerializedCollabPanel {
 //     Dismissed,
 // }
 
-// #[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
-// enum Section {
-//     ActiveCall,
-//     Channels,
-//     ChannelInvites,
-//     ContactRequests,
-//     Contacts,
-//     Online,
-//     Offline,
-// }
+#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
+enum Section {
+    ActiveCall,
+    Channels,
+    ChannelInvites,
+    ContactRequests,
+    Contacts,
+    Online,
+    Offline,
+}
 
-// #[derive(Clone, Debug)]
-// enum ListEntry {
-//     Header(Section),
-//     CallParticipant {
-//         user: Arc<User>,
-//         peer_id: Option<PeerId>,
-//         is_pending: bool,
-//     },
-//     ParticipantProject {
-//         project_id: u64,
-//         worktree_root_names: Vec<String>,
-//         host_user_id: u64,
-//         is_last: bool,
-//     },
-//     ParticipantScreen {
-//         peer_id: Option<PeerId>,
-//         is_last: bool,
-//     },
-//     IncomingRequest(Arc<User>),
-//     OutgoingRequest(Arc<User>),
-//     ChannelInvite(Arc<Channel>),
-//     Channel {
-//         channel: Arc<Channel>,
-//         depth: usize,
-//         has_children: bool,
-//     },
-//     ChannelNotes {
-//         channel_id: ChannelId,
-//     },
-//     ChannelChat {
-//         channel_id: ChannelId,
-//     },
-//     ChannelEditor {
-//         depth: usize,
-//     },
-//     Contact {
-//         contact: Arc<Contact>,
-//         calling: bool,
-//     },
-//     ContactPlaceholder,
-// }
+#[derive(Clone, Debug)]
+enum ListEntry {
+    Header(Section),
+    //     CallParticipant {
+    //         user: Arc<User>,
+    //         peer_id: Option<PeerId>,
+    //         is_pending: bool,
+    //     },
+    //     ParticipantProject {
+    //         project_id: u64,
+    //         worktree_root_names: Vec<String>,
+    //         host_user_id: u64,
+    //         is_last: bool,
+    //     },
+    //     ParticipantScreen {
+    //         peer_id: Option<PeerId>,
+    //         is_last: bool,
+    //     },
+    IncomingRequest(Arc<User>),
+    OutgoingRequest(Arc<User>),
+    //     ChannelInvite(Arc<Channel>),
+    Channel {
+        channel: Arc<Channel>,
+        depth: usize,
+        has_children: bool,
+    },
+    //     ChannelNotes {
+    //         channel_id: ChannelId,
+    //     },
+    //     ChannelChat {
+    //         channel_id: ChannelId,
+    //     },
+    ChannelEditor {
+        depth: usize,
+    },
+    Contact {
+        contact: Arc<Contact>,
+        calling: bool,
+    },
+    ContactPlaceholder,
+}
 
 // impl Entity for CollabPanel {
 //     type Event = Event;
@@ -397,16 +409,11 @@ impl CollabPanel {
         cx.build_view(|cx| {
             //             let view_id = cx.view_id();
 
-            //             let filter_editor = cx.add_view(|cx| {
-            //                 let mut editor = Editor::single_line(
-            //                     Some(Arc::new(|theme| {
-            //                         theme.collab_panel.user_query_editor.clone()
-            //                     })),
-            //                     cx,
-            //                 );
-            //                 editor.set_placeholder_text("Filter channels, contacts", cx);
-            //                 editor
-            //             });
+            let filter_editor = cx.build_view(|cx| {
+                let mut editor = Editor::single_line(cx);
+                editor.set_placeholder_text("Filter channels, contacts", cx);
+                editor
+            });
 
             //             cx.subscribe(&filter_editor, |this, _, event, cx| {
             //                 if let editor::Event::BufferEdited = event {
@@ -585,7 +592,7 @@ impl CollabPanel {
             //                     }
             //                 });
 
-            let this = Self {
+            let mut this = Self {
                 width: None,
                 focus_handle: cx.focus_handle(),
                 //                 channel_clipboard: None,
@@ -593,25 +600,25 @@ impl CollabPanel {
                 //                 pending_serialization: Task::ready(None),
                 //                 context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
                 //                 channel_name_editor,
-                //                 filter_editor,
-                //                 entries: Vec::default(),
-                //                 channel_editing_state: None,
-                //                 selection: None,
+                filter_editor,
+                entries: Vec::default(),
+                channel_editing_state: None,
+                selection: None,
+                channel_store: ChannelStore::global(cx),
                 user_store: workspace.user_store().clone(),
-                //                 channel_store: ChannelStore::global(cx),
                 //                 project: workspace.project().clone(),
-                //                 subscriptions: Vec::default(),
-                //                 match_candidates: Vec::default(),
-                //                 collapsed_sections: vec![Section::Offline],
-                //                 collapsed_channels: Vec::default(),
-                _workspace: workspace.weak_handle(),
-                _client: workspace.app_state().client.clone(),
+                subscriptions: Vec::default(),
+                match_candidates: Vec::default(),
+                collapsed_sections: vec![Section::Offline],
+                collapsed_channels: Vec::default(),
+                workspace: workspace.weak_handle(),
+                client: workspace.app_state().client.clone(),
                 //                 context_menu_on_selected: true,
                 //                 drag_target_channel: ChannelDragTarget::None,
                 //                 list_state,
             };
 
-            //             this.update_entries(false, cx);
+            this.update_entries(false, cx);
 
             //             // Update the dock position when the setting changes.
             //             let mut old_dock_position = this.position(cx);
@@ -628,10 +635,10 @@ impl CollabPanel {
             //                 );
 
             //             let active_call = ActiveCall::global(cx);
-            //             this.subscriptions
-            //                 .push(cx.observe(&this.user_store, |this, _, cx| {
-            //                     this.update_entries(true, cx)
-            //                 }));
+            this.subscriptions
+                .push(cx.observe(&this.user_store, |this, _, cx| {
+                    this.update_entries(true, cx)
+                }));
             //             this.subscriptions
             //                 .push(cx.observe(&this.channel_store, |this, _, cx| {
             //                     this.update_entries(true, cx)
@@ -720,449 +727,449 @@ impl CollabPanel {
     //         );
     //     }
 
-    //     fn update_entries(&mut self, select_same_item: bool, cx: &mut ViewContext<Self>) {
-    //         let channel_store = self.channel_store.read(cx);
-    //         let user_store = self.user_store.read(cx);
-    //         let query = self.filter_editor.read(cx).text(cx);
-    //         let executor = cx.background().clone();
-
-    //         let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned());
-    //         let old_entries = mem::take(&mut self.entries);
-    //         let mut scroll_to_top = false;
-
-    //         if let Some(room) = ActiveCall::global(cx).read(cx).room() {
-    //             self.entries.push(ListEntry::Header(Section::ActiveCall));
-    //             if !old_entries
-    //                 .iter()
-    //                 .any(|entry| matches!(entry, ListEntry::Header(Section::ActiveCall)))
-    //             {
-    //                 scroll_to_top = true;
-    //             }
-
-    //             if !self.collapsed_sections.contains(&Section::ActiveCall) {
-    //                 let room = room.read(cx);
-
-    //                 if let Some(channel_id) = room.channel_id() {
-    //                     self.entries.push(ListEntry::ChannelNotes { channel_id });
-    //                     self.entries.push(ListEntry::ChannelChat { channel_id })
-    //                 }
-
-    //                 // Populate the active user.
-    //                 if let Some(user) = user_store.current_user() {
-    //                     self.match_candidates.clear();
-    //                     self.match_candidates.push(StringMatchCandidate {
-    //                         id: 0,
-    //                         string: user.github_login.clone(),
-    //                         char_bag: user.github_login.chars().collect(),
-    //                     });
-    //                     let matches = executor.block(match_strings(
-    //                         &self.match_candidates,
-    //                         &query,
-    //                         true,
-    //                         usize::MAX,
-    //                         &Default::default(),
-    //                         executor.clone(),
-    //                     ));
-    //                     if !matches.is_empty() {
-    //                         let user_id = user.id;
-    //                         self.entries.push(ListEntry::CallParticipant {
-    //                             user,
-    //                             peer_id: None,
-    //                             is_pending: false,
-    //                         });
-    //                         let mut projects = room.local_participant().projects.iter().peekable();
-    //                         while let Some(project) = projects.next() {
-    //                             self.entries.push(ListEntry::ParticipantProject {
-    //                                 project_id: project.id,
-    //                                 worktree_root_names: project.worktree_root_names.clone(),
-    //                                 host_user_id: user_id,
-    //                                 is_last: projects.peek().is_none() && !room.is_screen_sharing(),
-    //                             });
-    //                         }
-    //                         if room.is_screen_sharing() {
-    //                             self.entries.push(ListEntry::ParticipantScreen {
-    //                                 peer_id: None,
-    //                                 is_last: true,
-    //                             });
-    //                         }
-    //                     }
-    //                 }
-
-    //                 // Populate remote participants.
-    //                 self.match_candidates.clear();
-    //                 self.match_candidates
-    //                     .extend(room.remote_participants().iter().map(|(_, participant)| {
-    //                         StringMatchCandidate {
-    //                             id: participant.user.id as usize,
-    //                             string: participant.user.github_login.clone(),
-    //                             char_bag: participant.user.github_login.chars().collect(),
-    //                         }
-    //                     }));
-    //                 let matches = executor.block(match_strings(
-    //                     &self.match_candidates,
-    //                     &query,
-    //                     true,
-    //                     usize::MAX,
-    //                     &Default::default(),
-    //                     executor.clone(),
-    //                 ));
-    //                 for mat in matches {
-    //                     let user_id = mat.candidate_id as u64;
-    //                     let participant = &room.remote_participants()[&user_id];
-    //                     self.entries.push(ListEntry::CallParticipant {
-    //                         user: participant.user.clone(),
-    //                         peer_id: Some(participant.peer_id),
-    //                         is_pending: false,
-    //                     });
-    //                     let mut projects = participant.projects.iter().peekable();
-    //                     while let Some(project) = projects.next() {
-    //                         self.entries.push(ListEntry::ParticipantProject {
-    //                             project_id: project.id,
-    //                             worktree_root_names: project.worktree_root_names.clone(),
-    //                             host_user_id: participant.user.id,
-    //                             is_last: projects.peek().is_none()
-    //                                 && participant.video_tracks.is_empty(),
-    //                         });
-    //                     }
-    //                     if !participant.video_tracks.is_empty() {
-    //                         self.entries.push(ListEntry::ParticipantScreen {
-    //                             peer_id: Some(participant.peer_id),
-    //                             is_last: true,
-    //                         });
-    //                     }
-    //                 }
-
-    //                 // Populate pending participants.
-    //                 self.match_candidates.clear();
-    //                 self.match_candidates
-    //                     .extend(room.pending_participants().iter().enumerate().map(
-    //                         |(id, participant)| StringMatchCandidate {
-    //                             id,
-    //                             string: participant.github_login.clone(),
-    //                             char_bag: participant.github_login.chars().collect(),
-    //                         },
-    //                     ));
-    //                 let matches = executor.block(match_strings(
-    //                     &self.match_candidates,
-    //                     &query,
-    //                     true,
-    //                     usize::MAX,
-    //                     &Default::default(),
-    //                     executor.clone(),
-    //                 ));
-    //                 self.entries
-    //                     .extend(matches.iter().map(|mat| ListEntry::CallParticipant {
-    //                         user: room.pending_participants()[mat.candidate_id].clone(),
-    //                         peer_id: None,
-    //                         is_pending: true,
-    //                     }));
-    //             }
-    //         }
-
-    //         let mut request_entries = Vec::new();
-
-    //         if cx.has_flag::<ChannelsAlpha>() {
-    //             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 {
-    //                             id: ix,
-    //                             string: channel.name.clone(),
-    //                             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 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 {
-    //                             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])
-    //                         });
-
-    //                     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(),
-    //                             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);
-    //                     }
-    //                 }
-    //             }
-    //         }
-
-    //         self.entries.push(ListEntry::Header(Section::Contacts));
-
-    //         request_entries.clear();
-    //         let incoming = user_store.incoming_contact_requests();
-    //         if !incoming.is_empty() {
-    //             self.match_candidates.clear();
-    //             self.match_candidates
-    //                 .extend(
-    //                     incoming
-    //                         .iter()
-    //                         .enumerate()
-    //                         .map(|(ix, user)| StringMatchCandidate {
-    //                             id: ix,
-    //                             string: user.github_login.clone(),
-    //                             char_bag: user.github_login.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::IncomingRequest(incoming[mat.candidate_id].clone())),
-    //             );
-    //         }
-
-    //         let outgoing = user_store.outgoing_contact_requests();
-    //         if !outgoing.is_empty() {
-    //             self.match_candidates.clear();
-    //             self.match_candidates
-    //                 .extend(
-    //                     outgoing
-    //                         .iter()
-    //                         .enumerate()
-    //                         .map(|(ix, user)| StringMatchCandidate {
-    //                             id: ix,
-    //                             string: user.github_login.clone(),
-    //                             char_bag: user.github_login.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::OutgoingRequest(outgoing[mat.candidate_id].clone())),
-    //             );
-    //         }
-
-    //         if !request_entries.is_empty() {
-    //             self.entries
-    //                 .push(ListEntry::Header(Section::ContactRequests));
-    //             if !self.collapsed_sections.contains(&Section::ContactRequests) {
-    //                 self.entries.append(&mut request_entries);
-    //             }
-    //         }
-
-    //         let contacts = user_store.contacts();
-    //         if !contacts.is_empty() {
-    //             self.match_candidates.clear();
-    //             self.match_candidates
-    //                 .extend(
-    //                     contacts
-    //                         .iter()
-    //                         .enumerate()
-    //                         .map(|(ix, contact)| StringMatchCandidate {
-    //                             id: ix,
-    //                             string: contact.user.github_login.clone(),
-    //                             char_bag: contact.user.github_login.chars().collect(),
-    //                         }),
-    //                 );
-
-    //             let matches = executor.block(match_strings(
-    //                 &self.match_candidates,
-    //                 &query,
-    //                 true,
-    //                 usize::MAX,
-    //                 &Default::default(),
-    //                 executor.clone(),
-    //             ));
-
-    //             let (online_contacts, offline_contacts) = matches
-    //                 .iter()
-    //                 .partition::<Vec<_>, _>(|mat| contacts[mat.candidate_id].online);
-
-    //             for (matches, section) in [
-    //                 (online_contacts, Section::Online),
-    //                 (offline_contacts, Section::Offline),
-    //             ] {
-    //                 if !matches.is_empty() {
-    //                     self.entries.push(ListEntry::Header(section));
-    //                     if !self.collapsed_sections.contains(&section) {
-    //                         let active_call = &ActiveCall::global(cx).read(cx);
-    //                         for mat in matches {
-    //                             let contact = &contacts[mat.candidate_id];
-    //                             self.entries.push(ListEntry::Contact {
-    //                                 contact: contact.clone(),
-    //                                 calling: active_call.pending_invites().contains(&contact.user.id),
-    //                             });
-    //                         }
-    //                     }
-    //                 }
-    //             }
-    //         }
-
-    //         if incoming.is_empty() && outgoing.is_empty() && contacts.is_empty() {
-    //             self.entries.push(ListEntry::ContactPlaceholder);
-    //         }
-
-    //         if select_same_item {
-    //             if let Some(prev_selected_entry) = prev_selected_entry {
-    //                 self.selection.take();
-    //                 for (ix, entry) in self.entries.iter().enumerate() {
-    //                     if *entry == prev_selected_entry {
-    //                         self.selection = Some(ix);
-    //                         break;
-    //                     }
-    //                 }
-    //             }
-    //         } else {
-    //             self.selection = self.selection.and_then(|prev_selection| {
-    //                 if self.entries.is_empty() {
-    //                     None
-    //                 } else {
-    //                     Some(prev_selection.min(self.entries.len() - 1))
-    //                 }
-    //             });
-    //         }
-
-    //         let old_scroll_top = self.list_state.logical_scroll_top();
-
-    //         self.list_state.reset(self.entries.len());
+    fn update_entries(&mut self, select_same_item: bool, cx: &mut ViewContext<Self>) {
+        let channel_store = self.channel_store.read(cx);
+        let user_store = self.user_store.read(cx);
+        let query = self.filter_editor.read(cx).text(cx);
+        let executor = cx.background_executor().clone();
+
+        // let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned());
+        let _old_entries = mem::take(&mut self.entries);
+        //         let mut scroll_to_top = false;
+
+        //         if let Some(room) = ActiveCall::global(cx).read(cx).room() {
+        //             self.entries.push(ListEntry::Header(Section::ActiveCall));
+        //             if !old_entries
+        //                 .iter()
+        //                 .any(|entry| matches!(entry, ListEntry::Header(Section::ActiveCall)))
+        //             {
+        //                 scroll_to_top = true;
+        //             }
+
+        //             if !self.collapsed_sections.contains(&Section::ActiveCall) {
+        //                 let room = room.read(cx);
+
+        //                 if let Some(channel_id) = room.channel_id() {
+        //                     self.entries.push(ListEntry::ChannelNotes { channel_id });
+        //                     self.entries.push(ListEntry::ChannelChat { channel_id })
+        //                 }
+
+        //                 // Populate the active user.
+        //                 if let Some(user) = user_store.current_user() {
+        //                     self.match_candidates.clear();
+        //                     self.match_candidates.push(StringMatchCandidate {
+        //                         id: 0,
+        //                         string: user.github_login.clone(),
+        //                         char_bag: user.github_login.chars().collect(),
+        //                     });
+        //                     let matches = executor.block(match_strings(
+        //                         &self.match_candidates,
+        //                         &query,
+        //                         true,
+        //                         usize::MAX,
+        //                         &Default::default(),
+        //                         executor.clone(),
+        //                     ));
+        //                     if !matches.is_empty() {
+        //                         let user_id = user.id;
+        //                         self.entries.push(ListEntry::CallParticipant {
+        //                             user,
+        //                             peer_id: None,
+        //                             is_pending: false,
+        //                         });
+        //                         let mut projects = room.local_participant().projects.iter().peekable();
+        //                         while let Some(project) = projects.next() {
+        //                             self.entries.push(ListEntry::ParticipantProject {
+        //                                 project_id: project.id,
+        //                                 worktree_root_names: project.worktree_root_names.clone(),
+        //                                 host_user_id: user_id,
+        //                                 is_last: projects.peek().is_none() && !room.is_screen_sharing(),
+        //                             });
+        //                         }
+        //                         if room.is_screen_sharing() {
+        //                             self.entries.push(ListEntry::ParticipantScreen {
+        //                                 peer_id: None,
+        //                                 is_last: true,
+        //                             });
+        //                         }
+        //                     }
+        //                 }
+
+        //                 // Populate remote participants.
+        //                 self.match_candidates.clear();
+        //                 self.match_candidates
+        //                     .extend(room.remote_participants().iter().map(|(_, participant)| {
+        //                         StringMatchCandidate {
+        //                             id: participant.user.id as usize,
+        //                             string: participant.user.github_login.clone(),
+        //                             char_bag: participant.user.github_login.chars().collect(),
+        //                         }
+        //                     }));
+        //                 let matches = executor.block(match_strings(
+        //                     &self.match_candidates,
+        //                     &query,
+        //                     true,
+        //                     usize::MAX,
+        //                     &Default::default(),
+        //                     executor.clone(),
+        //                 ));
+        //                 for mat in matches {
+        //                     let user_id = mat.candidate_id as u64;
+        //                     let participant = &room.remote_participants()[&user_id];
+        //                     self.entries.push(ListEntry::CallParticipant {
+        //                         user: participant.user.clone(),
+        //                         peer_id: Some(participant.peer_id),
+        //                         is_pending: false,
+        //                     });
+        //                     let mut projects = participant.projects.iter().peekable();
+        //                     while let Some(project) = projects.next() {
+        //                         self.entries.push(ListEntry::ParticipantProject {
+        //                             project_id: project.id,
+        //                             worktree_root_names: project.worktree_root_names.clone(),
+        //                             host_user_id: participant.user.id,
+        //                             is_last: projects.peek().is_none()
+        //                                 && participant.video_tracks.is_empty(),
+        //                         });
+        //                     }
+        //                     if !participant.video_tracks.is_empty() {
+        //                         self.entries.push(ListEntry::ParticipantScreen {
+        //                             peer_id: Some(participant.peer_id),
+        //                             is_last: true,
+        //                         });
+        //                     }
+        //                 }
+
+        //                 // Populate pending participants.
+        //                 self.match_candidates.clear();
+        //                 self.match_candidates
+        //                     .extend(room.pending_participants().iter().enumerate().map(
+        //                         |(id, participant)| StringMatchCandidate {
+        //                             id,
+        //                             string: participant.github_login.clone(),
+        //                             char_bag: participant.github_login.chars().collect(),
+        //                         },
+        //                     ));
+        //                 let matches = executor.block(match_strings(
+        //                     &self.match_candidates,
+        //                     &query,
+        //                     true,
+        //                     usize::MAX,
+        //                     &Default::default(),
+        //                     executor.clone(),
+        //                 ));
+        //                 self.entries
+        //                     .extend(matches.iter().map(|mat| ListEntry::CallParticipant {
+        //                         user: room.pending_participants()[mat.candidate_id].clone(),
+        //                         peer_id: None,
+        //                         is_pending: true,
+        //                     }));
+        //             }
+        //         }
+
+        let mut request_entries = Vec::new();
+
+        if cx.has_flag::<ChannelsAlpha>() {
+            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 {
+                            id: ix,
+                            string: channel.name.clone(),
+                            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 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 {
+                            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])
+                        });
+
+                    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,
+                            });
+                        }
+                    }
+                }
+            }
 
-    //         if scroll_to_top {
-    //             self.list_state.scroll_to(ListOffset::default());
-    //         } else {
-    //             // Attempt to maintain the same scroll position.
-    //             if let Some(old_top_entry) = old_entries.get(old_scroll_top.item_ix) {
-    //                 let new_scroll_top = self
-    //                     .entries
-    //                     .iter()
-    //                     .position(|entry| entry == old_top_entry)
-    //                     .map(|item_ix| ListOffset {
-    //                         item_ix,
-    //                         offset_in_item: old_scroll_top.offset_in_item,
-    //                     })
-    //                     .or_else(|| {
-    //                         let entry_after_old_top = old_entries.get(old_scroll_top.item_ix + 1)?;
-    //                         let item_ix = self
-    //                             .entries
-    //                             .iter()
-    //                             .position(|entry| entry == entry_after_old_top)?;
-    //                         Some(ListOffset {
-    //                             item_ix,
-    //                             offset_in_item: 0.,
-    //                         })
-    //                     })
-    //                     .or_else(|| {
-    //                         let entry_before_old_top =
-    //                             old_entries.get(old_scroll_top.item_ix.saturating_sub(1))?;
-    //                         let item_ix = self
-    //                             .entries
-    //                             .iter()
-    //                             .position(|entry| entry == entry_before_old_top)?;
-    //                         Some(ListOffset {
-    //                             item_ix,
-    //                             offset_in_item: 0.,
-    //                         })
-    //                     });
+            //             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(),
+            //                             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())
+            //                 }));
 
-    //                 self.list_state
-    //                     .scroll_to(new_scroll_top.unwrap_or(old_scroll_top));
-    //             }
-    //         }
+            //                 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);
+            //                     }
+            //                 }
+            //             }
+        }
+
+        self.entries.push(ListEntry::Header(Section::Contacts));
+
+        request_entries.clear();
+        let incoming = user_store.incoming_contact_requests();
+        if !incoming.is_empty() {
+            self.match_candidates.clear();
+            self.match_candidates
+                .extend(
+                    incoming
+                        .iter()
+                        .enumerate()
+                        .map(|(ix, user)| StringMatchCandidate {
+                            id: ix,
+                            string: user.github_login.clone(),
+                            char_bag: user.github_login.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::IncomingRequest(incoming[mat.candidate_id].clone())),
+            );
+        }
+
+        let outgoing = user_store.outgoing_contact_requests();
+        if !outgoing.is_empty() {
+            self.match_candidates.clear();
+            self.match_candidates
+                .extend(
+                    outgoing
+                        .iter()
+                        .enumerate()
+                        .map(|(ix, user)| StringMatchCandidate {
+                            id: ix,
+                            string: user.github_login.clone(),
+                            char_bag: user.github_login.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::OutgoingRequest(outgoing[mat.candidate_id].clone())),
+            );
+        }
+
+        if !request_entries.is_empty() {
+            self.entries
+                .push(ListEntry::Header(Section::ContactRequests));
+            if !self.collapsed_sections.contains(&Section::ContactRequests) {
+                self.entries.append(&mut request_entries);
+            }
+        }
+
+        let contacts = user_store.contacts();
+        if !contacts.is_empty() {
+            self.match_candidates.clear();
+            self.match_candidates
+                .extend(
+                    contacts
+                        .iter()
+                        .enumerate()
+                        .map(|(ix, contact)| StringMatchCandidate {
+                            id: ix,
+                            string: contact.user.github_login.clone(),
+                            char_bag: contact.user.github_login.chars().collect(),
+                        }),
+                );
+
+            let matches = executor.block(match_strings(
+                &self.match_candidates,
+                &query,
+                true,
+                usize::MAX,
+                &Default::default(),
+                executor.clone(),
+            ));
+
+            let (online_contacts, offline_contacts) = matches
+                .iter()
+                .partition::<Vec<_>, _>(|mat| contacts[mat.candidate_id].online);
+
+            for (matches, section) in [
+                (online_contacts, Section::Online),
+                (offline_contacts, Section::Offline),
+            ] {
+                if !matches.is_empty() {
+                    self.entries.push(ListEntry::Header(section));
+                    if !self.collapsed_sections.contains(&section) {
+                        let active_call = &ActiveCall::global(cx).read(cx);
+                        for mat in matches {
+                            let contact = &contacts[mat.candidate_id];
+                            self.entries.push(ListEntry::Contact {
+                                contact: contact.clone(),
+                                calling: active_call.pending_invites().contains(&contact.user.id),
+                            });
+                        }
+                    }
+                }
+            }
+        }
+
+        if incoming.is_empty() && outgoing.is_empty() && contacts.is_empty() {
+            self.entries.push(ListEntry::ContactPlaceholder);
+        }
+
+        //         if select_same_item {
+        //             if let Some(prev_selected_entry) = prev_selected_entry {
+        //                 self.selection.take();
+        //                 for (ix, entry) in self.entries.iter().enumerate() {
+        //                     if *entry == prev_selected_entry {
+        //                         self.selection = Some(ix);
+        //                         break;
+        //                     }
+        //                 }
+        //             }
+        //         } else {
+        //             self.selection = self.selection.and_then(|prev_selection| {
+        //                 if self.entries.is_empty() {
+        //                     None
+        //                 } else {
+        //                     Some(prev_selection.min(self.entries.len() - 1))
+        //                 }
+        //             });
+        //         }
+
+        //         let old_scroll_top = self.list_state.logical_scroll_top();
+
+        //         self.list_state.reset(self.entries.len());
+
+        //         if scroll_to_top {
+        //             self.list_state.scroll_to(ListOffset::default());
+        //         } else {
+        //             // Attempt to maintain the same scroll position.
+        //             if let Some(old_top_entry) = old_entries.get(old_scroll_top.item_ix) {
+        //                 let new_scroll_top = self
+        //                     .entries
+        //                     .iter()
+        //                     .position(|entry| entry == old_top_entry)
+        //                     .map(|item_ix| ListOffset {
+        //                         item_ix,
+        //                         offset_in_item: old_scroll_top.offset_in_item,
+        //                     })
+        //                     .or_else(|| {
+        //                         let entry_after_old_top = old_entries.get(old_scroll_top.item_ix + 1)?;
+        //                         let item_ix = self
+        //                             .entries
+        //                             .iter()
+        //                             .position(|entry| entry == entry_after_old_top)?;
+        //                         Some(ListOffset {
+        //                             item_ix,
+        //                             offset_in_item: 0.,
+        //                         })
+        //                     })
+        //                     .or_else(|| {
+        //                         let entry_before_old_top =
+        //                             old_entries.get(old_scroll_top.item_ix.saturating_sub(1))?;
+        //                         let item_ix = self
+        //                             .entries
+        //                             .iter()
+        //                             .position(|entry| entry == entry_before_old_top)?;
+        //                         Some(ListOffset {
+        //                             item_ix,
+        //                             offset_in_item: 0.,
+        //                         })
+        //                     });
+
+        //                 self.list_state
+        //                     .scroll_to(new_scroll_top.unwrap_or(old_scroll_top));
+        //             }
+        //         }
 
-    //         cx.notify();
-    //     }
+        cx.notify();
+    }
 
     //     fn render_call_participant(
     //         user: &User,

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

@@ -1,37 +1,34 @@
 use client::{ContactRequestStatus, User, UserStore};
 use gpui::{
-    elements::*, AppContext, Entity, ModelHandle, MouseState, Task, View, ViewContext, ViewHandle,
+    div, img, svg, AnyElement, AppContext, DismissEvent, Div, Entity, EventEmitter, FocusHandle,
+    FocusableView, Img, IntoElement, Model, ParentElement as _, Render, Styled, Task, View,
+    ViewContext, VisualContext, WeakView,
 };
-use picker::{Picker, PickerDelegate, PickerEvent};
+use picker::{Picker, PickerDelegate};
 use std::sync::Arc;
-use util::TryFutureExt;
-use workspace::Modal;
+use theme::ActiveTheme as _;
+use ui::{h_stack, v_stack, Label};
+use util::{ResultExt as _, TryFutureExt};
 
 pub fn init(cx: &mut AppContext) {
-    Picker::<ContactFinderDelegate>::init(cx);
-    cx.add_action(ContactFinder::dismiss)
+    //Picker::<ContactFinderDelegate>::init(cx);
+    //cx.add_action(ContactFinder::dismiss)
 }
 
 pub struct ContactFinder {
-    picker: ViewHandle<Picker<ContactFinderDelegate>>,
+    picker: View<Picker<ContactFinderDelegate>>,
     has_focus: bool,
 }
 
 impl ContactFinder {
-    pub fn new(user_store: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) -> Self {
-        let picker = cx.add_view(|cx| {
-            Picker::new(
-                ContactFinderDelegate {
-                    user_store,
-                    potential_contacts: Arc::from([]),
-                    selected_index: 0,
-                },
-                cx,
-            )
-            .with_theme(|theme| theme.collab_panel.tabbed_modal.picker.clone())
-        });
-
-        cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach();
+    pub fn new(user_store: Model<UserStore>, cx: &mut ViewContext<Self>) -> Self {
+        let delegate = ContactFinderDelegate {
+            parent: cx.view().downgrade(),
+            user_store,
+            potential_contacts: Arc::from([]),
+            selected_index: 0,
+        };
+        let picker = cx.build_view(|cx| Picker::new(delegate, cx));
 
         Self {
             picker,
@@ -41,105 +38,72 @@ impl ContactFinder {
 
     pub fn set_query(&mut self, query: String, cx: &mut ViewContext<Self>) {
         self.picker.update(cx, |picker, cx| {
-            picker.set_query(query, cx);
+            // todo!()
+            // picker.set_query(query, cx);
         });
     }
-
-    fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
-        cx.emit(PickerEvent::Dismiss);
-    }
-}
-
-impl Entity for ContactFinder {
-    type Event = PickerEvent;
 }
 
-impl View for ContactFinder {
-    fn ui_name() -> &'static str {
-        "ContactFinder"
-    }
-
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let full_theme = &theme::current(cx);
-        let theme = &full_theme.collab_panel.tabbed_modal;
-
-        fn render_mode_button(
-            text: &'static str,
-            theme: &theme::TabbedModal,
-            _cx: &mut ViewContext<ContactFinder>,
-        ) -> AnyElement<ContactFinder> {
-            let contained_text = &theme.tab_button.active_state().default;
-            Label::new(text, contained_text.text.clone())
-                .contained()
-                .with_style(contained_text.container.clone())
-                .into_any()
+impl Render for ContactFinder {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+        fn render_mode_button(text: &'static str) -> AnyElement {
+            Label::new(text).into_any_element()
         }
 
-        Flex::column()
-            .with_child(
-                Flex::column()
-                    .with_child(
-                        Label::new("Contacts", theme.title.text.clone())
-                            .contained()
-                            .with_style(theme.title.container.clone()),
-                    )
-                    .with_child(Flex::row().with_children([render_mode_button(
-                        "Invite new contacts",
-                        &theme,
-                        cx,
-                    )]))
-                    .expanded()
-                    .contained()
-                    .with_style(theme.header),
-            )
-            .with_child(
-                ChildView::new(&self.picker, cx)
-                    .contained()
-                    .with_style(theme.body),
+        v_stack()
+            .child(
+                v_stack()
+                    .child(Label::new("Contacts"))
+                    .child(h_stack().children([render_mode_button("Invite new contacts")]))
+                    .bg(cx.theme().colors().element_background),
             )
-            .constrained()
-            .with_max_height(theme.max_height)
-            .with_max_width(theme.max_width)
-            .contained()
-            .with_style(theme.modal)
-            .into_any()
+            .child(self.picker.clone())
+            .w_96()
     }
 
-    fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
-        self.has_focus = true;
-        if cx.is_self_focused() {
-            cx.focus(&self.picker)
-        }
-    }
+    // fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
+    //     self.has_focus = true;
+    //     if cx.is_self_focused() {
+    //         cx.focus(&self.picker)
+    //     }
+    // }
 
-    fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
-        self.has_focus = false;
-    }
+    // fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
+    //     self.has_focus = false;
+    // }
+
+    type Element = Div;
 }
 
-impl Modal for ContactFinder {
-    fn has_focus(&self) -> bool {
-        self.has_focus
-    }
+// impl Modal for ContactFinder {
+//     fn has_focus(&self) -> bool {
+//         self.has_focus
+//     }
 
-    fn dismiss_on_event(event: &Self::Event) -> bool {
-        match event {
-            PickerEvent::Dismiss => true,
-        }
-    }
-}
+//     fn dismiss_on_event(event: &Self::Event) -> bool {
+//         match event {
+//             PickerEvent::Dismiss => true,
+//         }
+//     }
+// }
 
 pub struct ContactFinderDelegate {
+    parent: WeakView<ContactFinder>,
     potential_contacts: Arc<[Arc<User>]>,
-    user_store: ModelHandle<UserStore>,
+    user_store: Model<UserStore>,
     selected_index: usize,
 }
 
-impl PickerDelegate for ContactFinderDelegate {
-    fn placeholder_text(&self) -> Arc<str> {
-        "Search collaborator by username...".into()
+impl EventEmitter<DismissEvent> for ContactFinder {}
+
+impl FocusableView for ContactFinder {
+    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+        self.picker.focus_handle(cx)
     }
+}
 
+impl PickerDelegate for ContactFinderDelegate {
+    type ListItem = Div;
     fn match_count(&self) -> usize {
         self.potential_contacts.len()
     }
@@ -152,6 +116,10 @@ impl PickerDelegate for ContactFinderDelegate {
         self.selected_index = ix;
     }
 
+    fn placeholder_text(&self) -> Arc<str> {
+        "Search collaborator by username...".into()
+    }
+
     fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
         let search_users = self
             .user_store
@@ -161,7 +129,7 @@ impl PickerDelegate for ContactFinderDelegate {
             async {
                 let potential_contacts = search_users.await?;
                 picker.update(&mut cx, |picker, cx| {
-                    picker.delegate_mut().potential_contacts = potential_contacts.into();
+                    picker.delegate.potential_contacts = potential_contacts.into();
                     cx.notify();
                 })?;
                 anyhow::Ok(())
@@ -191,19 +159,18 @@ impl PickerDelegate for ContactFinderDelegate {
     }
 
     fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
-        cx.emit(PickerEvent::Dismiss);
+        //cx.emit(PickerEvent::Dismiss);
+        self.parent
+            .update(cx, |_, cx| cx.emit(DismissEvent::Dismiss))
+            .log_err();
     }
 
     fn render_match(
         &self,
         ix: usize,
-        mouse_state: &mut MouseState,
         selected: bool,
-        cx: &gpui::AppContext,
-    ) -> AnyElement<Picker<Self>> {
-        let full_theme = &theme::current(cx);
-        let theme = &full_theme.collab_panel.contact_finder;
-        let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
+        cx: &mut ViewContext<Picker<Self>>,
+    ) -> Option<Self::ListItem> {
         let user = &self.potential_contacts[ix];
         let request_status = self.user_store.read(cx).contact_request_status(user);
 
@@ -214,48 +181,47 @@ impl PickerDelegate for ContactFinderDelegate {
             ContactRequestStatus::RequestSent => Some("icons/x.svg"),
             ContactRequestStatus::RequestAccepted => None,
         };
-        let button_style = if self.user_store.read(cx).is_contact_request_pending(user) {
-            &theme.disabled_contact_button
-        } else {
-            &theme.contact_button
-        };
-        let style = tabbed_modal
-            .picker
-            .item
-            .in_state(selected)
-            .style_for(mouse_state);
-        Flex::row()
-            .with_children(user.avatar.clone().map(|avatar| {
-                Image::from_data(avatar)
-                    .with_style(theme.contact_avatar)
-                    .aligned()
-                    .left()
-            }))
-            .with_child(
-                Label::new(user.github_login.clone(), style.label.clone())
-                    .contained()
-                    .with_style(theme.contact_username)
-                    .aligned()
-                    .left(),
-            )
-            .with_children(icon_path.map(|icon_path| {
-                Svg::new(icon_path)
-                    .with_color(button_style.color)
-                    .constrained()
-                    .with_width(button_style.icon_width)
-                    .aligned()
-                    .contained()
-                    .with_style(button_style.container)
-                    .constrained()
-                    .with_width(button_style.button_width)
-                    .with_height(button_style.button_width)
-                    .aligned()
-                    .flex_float()
-            }))
-            .contained()
-            .with_style(style.container)
-            .constrained()
-            .with_height(tabbed_modal.row_height)
-            .into_any()
+        dbg!(icon_path);
+        Some(
+            div()
+                .flex_1()
+                .justify_between()
+                .children(user.avatar.clone().map(|avatar| img().data(avatar)))
+                .child(Label::new(user.github_login.clone()))
+                .children(icon_path.map(|icon_path| svg().path(icon_path))),
+        )
+        // Flex::row()
+        //     .with_children(user.avatar.clone().map(|avatar| {
+        //         Image::from_data(avatar)
+        //             .with_style(theme.contact_avatar)
+        //             .aligned()
+        //             .left()
+        //     }))
+        //     .with_child(
+        //         Label::new(user.github_login.clone(), style.label.clone())
+        //             .contained()
+        //             .with_style(theme.contact_username)
+        //             .aligned()
+        //             .left(),
+        //     )
+        //     .with_children(icon_path.map(|icon_path| {
+        //         Svg::new(icon_path)
+        //             .with_color(button_style.color)
+        //             .constrained()
+        //             .with_width(button_style.icon_width)
+        //             .aligned()
+        //             .contained()
+        //             .with_style(button_style.container)
+        //             .constrained()
+        //             .with_width(button_style.button_width)
+        //             .with_height(button_style.button_width)
+        //             .aligned()
+        //             .flex_float()
+        //     }))
+        //     .contained()
+        //     .with_style(style.container)
+        //     .constrained()
+        //     .with_height(tabbed_modal.row_height)
+        //     .into_any()
     }
 }

crates/collab_ui2/src/collab_titlebar_item.rs 🔗

@@ -39,7 +39,7 @@ use project::Project;
 use theme::ActiveTheme;
 use ui::{h_stack, Avatar, Button, ButtonVariant, Color, IconButton, KeyBinding, Tooltip};
 use util::ResultExt;
-use workspace::Workspace;
+use workspace::{notifications::NotifyResultExt, Workspace};
 
 use crate::face_pile::FacePile;
 
@@ -290,11 +290,13 @@ impl Render for CollabTitlebarItem {
                 } else {
                     this.child(Button::new("Sign in").on_click(move |_, cx| {
                         let client = client.clone();
-                        cx.spawn(move |cx| async move {
-                            client.authenticate_and_connect(true, &cx).await?;
-                            Ok::<(), anyhow::Error>(())
+                        cx.spawn(move |mut cx| async move {
+                            client
+                                .authenticate_and_connect(true, &cx)
+                                .await
+                                .notify_async_err(&mut cx);
                         })
-                        .detach_and_log_err(cx);
+                        .detach();
                     }))
                 }
             })

crates/collab_ui2/src/face_pile.rs 🔗

@@ -3,8 +3,8 @@ use gpui::{
 };
 
 #[derive(Default)]
-pub(crate) struct FacePile {
-    faces: Vec<AnyElement>,
+pub struct FacePile {
+    pub faces: Vec<AnyElement>,
 }
 
 impl RenderOnce for FacePile {

crates/command_palette2/src/command_palette.rs 🔗

@@ -1,17 +1,17 @@
-use collections::{CommandPaletteFilter, HashMap};
-use fuzzy::{StringMatch, StringMatchCandidate};
-use gpui::{
-    actions, div, prelude::*, Action, AnyElement, AppContext, DismissEvent, Div, EventEmitter,
-    FocusHandle, FocusableView, Keystroke, ParentElement, Render, Styled, View, ViewContext,
-    VisualContext, WeakView,
-};
-use picker::{simple_picker_match, Picker, PickerDelegate};
 use std::{
     cmp::{self, Reverse},
     sync::Arc,
 };
 
-use ui::{h_stack, v_stack, HighlightedLabel, KeyBinding};
+use collections::{CommandPaletteFilter, HashMap};
+use fuzzy::{StringMatch, StringMatchCandidate};
+use gpui::{
+    actions, Action, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView,
+    Keystroke, ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView,
+};
+use picker::{Picker, PickerDelegate};
+
+use ui::{h_stack, v_stack, HighlightedLabel, KeyBinding, ListItem};
 use util::{
     channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL},
     ResultExt,
@@ -81,7 +81,7 @@ impl Render for CommandPalette {
     type Element = Div;
 
     fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
-        v_stack().w_96().child(self.picker.clone())
+        v_stack().min_w_96().child(self.picker.clone())
     }
 }
 
@@ -141,6 +141,8 @@ impl CommandPaletteDelegate {
 }
 
 impl PickerDelegate for CommandPaletteDelegate {
+    type ListItem = ListItem;
+
     fn placeholder_text(&self) -> Arc<str> {
         "Execute a command...".into()
     }
@@ -292,24 +294,26 @@ impl PickerDelegate for CommandPaletteDelegate {
         ix: usize,
         selected: bool,
         cx: &mut ViewContext<Picker<Self>>,
-    ) -> AnyElement {
+    ) -> Option<Self::ListItem> {
         let Some(r#match) = self.matches.get(ix) else {
-            return div().into_any();
+            return None;
         };
         let Some(command) = self.commands.get(r#match.candidate_id) else {
-            return div().into_any();
+            return None;
         };
 
-        simple_picker_match(selected, cx, |cx| {
-            h_stack()
-                .justify_between()
-                .child(HighlightedLabel::new(
-                    command.name.clone(),
-                    r#match.positions.clone(),
-                ))
-                .children(KeyBinding::for_action(&*command.action, cx))
-                .into_any()
-        })
+        Some(
+            ListItem::new(ix).inset(true).selected(selected).child(
+                h_stack()
+                    .w_full()
+                    .justify_between()
+                    .child(HighlightedLabel::new(
+                        command.name.clone(),
+                        r#match.positions.clone(),
+                    ))
+                    .children(KeyBinding::for_action(&*command.action, cx)),
+            ),
+        )
     }
 }
 

crates/editor2/src/editor.rs 🔗

@@ -1273,6 +1273,13 @@ impl CompletionsMenu {
             multiline_docs.map(|div| {
                 div.id("multiline_docs")
                     .max_h(max_height)
+                    .flex_1()
+                    .px_1p5()
+                    .py_1()
+                    .min_w(px(260.))
+                    .max_w(px(640.))
+                    .w(px(500.))
+                    .text_ui()
                     .overflow_y_scroll()
                     // Prevent a mouse down on documentation from being propagated to the editor,
                     // because that would move the cursor.
@@ -1327,13 +1334,18 @@ impl CompletionsMenu {
 
                         div()
                             .id(mat.candidate_id)
-                            .min_w(px(300.))
-                            .max_w(px(700.))
+                            .min_w(px(220.))
+                            .max_w(px(540.))
                             .whitespace_nowrap()
                             .overflow_hidden()
-                            .bg(gpui::green())
-                            .hover(|style| style.bg(gpui::blue()))
-                            .when(item_ix == selected_item, |div| div.bg(gpui::red()))
+                            .text_ui()
+                            .px_1()
+                            .rounded(px(4.))
+                            .bg(cx.theme().colors().ghost_element_background)
+                            .hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
+                            .when(item_ix == selected_item, |div| {
+                                div.bg(cx.theme().colors().ghost_element_selected)
+                            })
                             .on_mouse_down(
                                 MouseButton::Left,
                                 cx.listener(move |editor, event, cx| {

crates/editor2/src/element.rs 🔗

@@ -22,10 +22,11 @@ use collections::{BTreeMap, HashMap};
 use gpui::{
     div, point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace,
     BorrowWindow, Bounds, ContentMask, Corners, DispatchPhase, Edges, Element, ElementId,
-    ElementInputHandler, Entity, EntityId, Hsla, InteractiveElement, IntoElement, LineLayout,
-    MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, RenderOnce,
-    ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled,
-    TextRun, TextStyle, View, ViewContext, WeakView, WindowContext, WrappedLine,
+    ElementInputHandler, Entity, EntityId, Hsla, InteractiveBounds, InteractiveElement,
+    IntoElement, LineLayout, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
+    ParentElement, Pixels, RenderOnce, ScrollWheelEvent, ShapedLine, SharedString, Size,
+    StackingOrder, StatefulInteractiveElement, Style, Styled, TextRun, TextStyle, View,
+    ViewContext, WeakView, WindowContext, WrappedLine,
 };
 use itertools::Itertools;
 use language::language_settings::ShowWhitespaceSetting;
@@ -316,6 +317,7 @@ impl EditorElement {
         position_map: &PositionMap,
         text_bounds: Bounds<Pixels>,
         gutter_bounds: Bounds<Pixels>,
+        stacking_order: &StackingOrder,
         cx: &mut ViewContext<Editor>,
     ) -> bool {
         let mut click_count = event.click_count;
@@ -326,6 +328,9 @@ impl EditorElement {
         } else if !text_bounds.contains_point(&event.position) {
             return false;
         }
+        if !cx.was_top_layer(&event.position, stacking_order) {
+            return false;
+        }
 
         let point_for_position = position_map.point_for_position(text_bounds, event.position);
         let position = point_for_position.previous_valid;
@@ -384,6 +389,7 @@ impl EditorElement {
         event: &MouseUpEvent,
         position_map: &PositionMap,
         text_bounds: Bounds<Pixels>,
+        stacking_order: &StackingOrder,
         cx: &mut ViewContext<Editor>,
     ) -> bool {
         let end_selection = editor.has_pending_selection();
@@ -396,6 +402,7 @@ impl EditorElement {
         if !pending_nonempty_selections
             && event.modifiers.command
             && text_bounds.contains_point(&event.position)
+            && cx.was_top_layer(&event.position, stacking_order)
         {
             let point = position_map.point_for_position(text_bounds, event.position);
             let could_be_inlay = point.as_valid().is_none();
@@ -418,6 +425,7 @@ impl EditorElement {
         position_map: &PositionMap,
         text_bounds: Bounds<Pixels>,
         gutter_bounds: Bounds<Pixels>,
+        stacking_order: &StackingOrder,
         cx: &mut ViewContext<Editor>,
     ) -> bool {
         let modifiers = event.modifiers;
@@ -457,10 +465,12 @@ impl EditorElement {
 
         let text_hovered = text_bounds.contains_point(&event.position);
         let gutter_hovered = gutter_bounds.contains_point(&event.position);
+        let was_top = cx.was_top_layer(&event.position, stacking_order);
+
         editor.set_gutter_hovered(gutter_hovered, cx);
 
         // Don't trigger hover popover if mouse is hovering over context menu
-        if text_hovered {
+        if text_hovered && was_top {
             let point_for_position = position_map.point_for_position(text_bounds, event.position);
 
             match point_for_position.as_valid() {
@@ -490,7 +500,7 @@ impl EditorElement {
         } else {
             update_go_to_definition_link(editor, None, modifiers.command, modifiers.shift, cx);
             hover_at(editor, None, cx);
-            gutter_hovered
+            gutter_hovered && was_top
         }
     }
 
@@ -498,10 +508,10 @@ impl EditorElement {
         editor: &mut Editor,
         event: &ScrollWheelEvent,
         position_map: &PositionMap,
-        bounds: Bounds<Pixels>,
+        bounds: &InteractiveBounds,
         cx: &mut ViewContext<Editor>,
     ) -> bool {
-        if !bounds.contains_point(&event.position) {
+        if !bounds.visibly_contains(&event.position, cx) {
             return false;
         }
 
@@ -2282,10 +2292,15 @@ impl EditorElement {
         cx: &mut WindowContext,
     ) {
         let content_origin = text_bounds.origin + point(layout.gutter_margin, Pixels::ZERO);
+        let interactive_bounds = InteractiveBounds {
+            bounds: bounds.intersect(&cx.content_mask().bounds),
+            stacking_order: cx.stacking_order().clone(),
+        };
 
         cx.on_mouse_event({
             let position_map = layout.position_map.clone();
             let editor = self.editor.clone();
+            let interactive_bounds = interactive_bounds.clone();
 
             move |event: &ScrollWheelEvent, phase, cx| {
                 if phase != DispatchPhase::Bubble {
@@ -2293,7 +2308,7 @@ impl EditorElement {
                 }
 
                 let should_cancel = editor.update(cx, |editor, cx| {
-                    Self::scroll(editor, event, &position_map, bounds, cx)
+                    Self::scroll(editor, event, &position_map, &interactive_bounds, cx)
                 });
                 if should_cancel {
                     cx.stop_propagation();
@@ -2304,6 +2319,7 @@ impl EditorElement {
         cx.on_mouse_event({
             let position_map = layout.position_map.clone();
             let editor = self.editor.clone();
+            let stacking_order = cx.stacking_order().clone();
 
             move |event: &MouseDownEvent, phase, cx| {
                 if phase != DispatchPhase::Bubble {
@@ -2311,7 +2327,15 @@ impl EditorElement {
                 }
 
                 let should_cancel = editor.update(cx, |editor, cx| {
-                    Self::mouse_down(editor, event, &position_map, text_bounds, gutter_bounds, cx)
+                    Self::mouse_down(
+                        editor,
+                        event,
+                        &position_map,
+                        text_bounds,
+                        gutter_bounds,
+                        &stacking_order,
+                        cx,
+                    )
                 });
 
                 if should_cancel {
@@ -2323,9 +2347,18 @@ impl EditorElement {
         cx.on_mouse_event({
             let position_map = layout.position_map.clone();
             let editor = self.editor.clone();
+            let stacking_order = cx.stacking_order().clone();
+
             move |event: &MouseUpEvent, phase, cx| {
                 let should_cancel = editor.update(cx, |editor, cx| {
-                    Self::mouse_up(editor, event, &position_map, text_bounds, cx)
+                    Self::mouse_up(
+                        editor,
+                        event,
+                        &position_map,
+                        text_bounds,
+                        &stacking_order,
+                        cx,
+                    )
                 });
 
                 if should_cancel {
@@ -2351,13 +2384,23 @@ impl EditorElement {
         cx.on_mouse_event({
             let position_map = layout.position_map.clone();
             let editor = self.editor.clone();
+            let stacking_order = cx.stacking_order().clone();
+
             move |event: &MouseMoveEvent, phase, cx| {
                 if phase != DispatchPhase::Bubble {
                     return;
                 }
 
                 let stop_propogating = editor.update(cx, |editor, cx| {
-                    Self::mouse_moved(editor, event, &position_map, text_bounds, gutter_bounds, cx)
+                    Self::mouse_moved(
+                        editor,
+                        event,
+                        &position_map,
+                        text_bounds,
+                        gutter_bounds,
+                        &stacking_order,
+                        cx,
+                    )
                 });
 
                 if stop_propogating {
@@ -2617,9 +2660,11 @@ impl Element for EditorElement {
             // We call with_z_index to establish a new stacking context.
             cx.with_z_index(0, |cx| {
                 cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
-                    // Paint mouse listeners first, so any elements we paint on top of the editor
+                    // Paint mouse listeners at z-index 0 so any elements we paint on top of the editor
                     // take precedence.
-                    self.paint_mouse_listeners(bounds, gutter_bounds, text_bounds, &layout, cx);
+                    cx.with_z_index(0, |cx| {
+                        self.paint_mouse_listeners(bounds, gutter_bounds, text_bounds, &layout, cx);
+                    });
                     let input_handler = ElementInputHandler::new(bounds, self.editor.clone(), cx);
                     cx.handle_input(&focus_handle, input_handler);
 

crates/editor2/src/hover_popover.rs 🔗

@@ -483,9 +483,6 @@ impl InfoPopover {
             // Prevent a mouse move on the popover from being propagated to the editor,
             // because that would dismiss the popover.
             .on_mouse_move(|_, cx| cx.stop_propagation())
-            // Prevent a mouse down on the popover from being propagated to the editor,
-            // because that would move the cursor.
-            .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
             .child(crate::render_parsed_markdown(
                 "content",
                 &self.parsed_content,

crates/file_finder2/src/file_finder.rs 🔗

@@ -2,9 +2,8 @@ use collections::HashMap;
 use editor::{scroll::autoscroll::Autoscroll, Bias, Editor};
 use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
 use gpui::{
-    actions, div, AnyElement, AppContext, DismissEvent, Div, Element, EventEmitter, FocusHandle,
-    FocusableView, InteractiveElement, IntoElement, Model, ParentElement, Render, Styled, Task,
-    View, ViewContext, VisualContext, WeakView,
+    actions, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, Model,
+    ParentElement, Render, Styled, Task, View, ViewContext, VisualContext, WeakView,
 };
 use picker::{Picker, PickerDelegate};
 use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
@@ -16,8 +15,7 @@ use std::{
     },
 };
 use text::Point;
-use theme::ActiveTheme;
-use ui::{v_stack, HighlightedLabel, StyledExt};
+use ui::{v_stack, HighlightedLabel, ListItem};
 use util::{paths::PathLikeWithPosition, post_inc, ResultExt};
 use workspace::Workspace;
 
@@ -530,6 +528,8 @@ impl FileFinderDelegate {
 }
 
 impl PickerDelegate for FileFinderDelegate {
+    type ListItem = ListItem;
+
     fn placeholder_text(&self) -> Arc<str> {
         "Search project files...".into()
     }
@@ -709,31 +709,22 @@ impl PickerDelegate for FileFinderDelegate {
         ix: usize,
         selected: bool,
         cx: &mut ViewContext<Picker<Self>>,
-    ) -> AnyElement {
+    ) -> Option<Self::ListItem> {
         let path_match = self
             .matches
             .get(ix)
             .expect("Invalid matches state: no element for index {ix}");
-        let theme = cx.theme();
-        let colors = theme.colors();
 
         let (file_name, file_name_positions, full_path, full_path_positions) =
             self.labels_for_match(path_match, cx, ix);
 
-        div()
-            .px_1()
-            .text_color(colors.text)
-            .text_ui()
-            .bg(colors.ghost_element_background)
-            .rounded_md()
-            .when(selected, |this| this.bg(colors.ghost_element_selected))
-            .hover(|this| this.bg(colors.ghost_element_hover))
-            .child(
+        Some(
+            ListItem::new(ix).inset(true).selected(selected).child(
                 v_stack()
                     .child(HighlightedLabel::new(file_name, file_name_positions))
                     .child(HighlightedLabel::new(full_path, full_path_positions)),
-            )
-            .into_any()
+            ),
+        )
     }
 }
 

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

@@ -3,7 +3,8 @@ use crate::{
     BorrowWindow, Bounds, ClickEvent, DispatchPhase, Element, ElementId, FocusEvent, FocusHandle,
     IntoElement, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, MouseButton, MouseDownEvent,
     MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point, Render, ScrollWheelEvent,
-    SharedString, Size, Style, StyleRefinement, Styled, Task, View, Visibility, WindowContext,
+    SharedString, Size, StackingOrder, Style, StyleRefinement, Styled, Task, View, Visibility,
+    WindowContext,
 };
 use collections::HashMap;
 use refineable::Refineable;
@@ -84,7 +85,7 @@ pub trait InteractiveElement: Sized + Element {
             move |event, bounds, phase, cx| {
                 if phase == DispatchPhase::Bubble
                     && event.button == button
-                    && bounds.contains_point(&event.position)
+                    && bounds.visibly_contains(&event.position, cx)
                 {
                     (listener)(event, cx)
                 }
@@ -99,7 +100,7 @@ pub trait InteractiveElement: Sized + Element {
     ) -> Self {
         self.interactivity().mouse_down_listeners.push(Box::new(
             move |event, bounds, phase, cx| {
-                if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
+                if phase == DispatchPhase::Bubble && bounds.visibly_contains(&event.position, cx) {
                     (listener)(event, cx)
                 }
             },
@@ -117,7 +118,7 @@ pub trait InteractiveElement: Sized + Element {
             .push(Box::new(move |event, bounds, phase, cx| {
                 if phase == DispatchPhase::Bubble
                     && event.button == button
-                    && bounds.contains_point(&event.position)
+                    && bounds.visibly_contains(&event.position, cx)
                 {
                     (listener)(event, cx)
                 }
@@ -132,7 +133,7 @@ pub trait InteractiveElement: Sized + Element {
         self.interactivity()
             .mouse_up_listeners
             .push(Box::new(move |event, bounds, phase, cx| {
-                if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
+                if phase == DispatchPhase::Bubble && bounds.visibly_contains(&event.position, cx) {
                     (listener)(event, cx)
                 }
             }));
@@ -145,7 +146,8 @@ pub trait InteractiveElement: Sized + Element {
     ) -> Self {
         self.interactivity().mouse_down_listeners.push(Box::new(
             move |event, bounds, phase, cx| {
-                if phase == DispatchPhase::Capture && !bounds.contains_point(&event.position) {
+                if phase == DispatchPhase::Capture && !bounds.visibly_contains(&event.position, cx)
+                {
                     (listener)(event, cx)
                 }
             },
@@ -163,7 +165,7 @@ pub trait InteractiveElement: Sized + Element {
             .push(Box::new(move |event, bounds, phase, cx| {
                 if phase == DispatchPhase::Capture
                     && event.button == button
-                    && !bounds.contains_point(&event.position)
+                    && !bounds.visibly_contains(&event.position, cx)
                 {
                     (listener)(event, cx);
                 }
@@ -177,7 +179,7 @@ pub trait InteractiveElement: Sized + Element {
     ) -> Self {
         self.interactivity().mouse_move_listeners.push(Box::new(
             move |event, bounds, phase, cx| {
-                if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
+                if phase == DispatchPhase::Bubble && bounds.visibly_contains(&event.position, cx) {
                     (listener)(event, cx);
                 }
             },
@@ -191,7 +193,7 @@ pub trait InteractiveElement: Sized + Element {
     ) -> Self {
         self.interactivity().scroll_wheel_listeners.push(Box::new(
             move |event, bounds, phase, cx| {
-                if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
+                if phase == DispatchPhase::Bubble && bounds.visibly_contains(&event.position, cx) {
                     (listener)(event, cx);
                 }
             },
@@ -526,15 +528,15 @@ pub type FocusListeners = SmallVec<[FocusListener; 2]>;
 pub type FocusListener = Box<dyn Fn(&FocusHandle, &FocusEvent, &mut WindowContext) + 'static>;
 
 pub type MouseDownListener =
-    Box<dyn Fn(&MouseDownEvent, &Bounds<Pixels>, DispatchPhase, &mut WindowContext) + 'static>;
+    Box<dyn Fn(&MouseDownEvent, &InteractiveBounds, DispatchPhase, &mut WindowContext) + 'static>;
 pub type MouseUpListener =
-    Box<dyn Fn(&MouseUpEvent, &Bounds<Pixels>, DispatchPhase, &mut WindowContext) + 'static>;
+    Box<dyn Fn(&MouseUpEvent, &InteractiveBounds, DispatchPhase, &mut WindowContext) + 'static>;
 
 pub type MouseMoveListener =
-    Box<dyn Fn(&MouseMoveEvent, &Bounds<Pixels>, DispatchPhase, &mut WindowContext) + 'static>;
+    Box<dyn Fn(&MouseMoveEvent, &InteractiveBounds, DispatchPhase, &mut WindowContext) + 'static>;
 
 pub type ScrollWheelListener =
-    Box<dyn Fn(&ScrollWheelEvent, &Bounds<Pixels>, DispatchPhase, &mut WindowContext) + 'static>;
+    Box<dyn Fn(&ScrollWheelEvent, &InteractiveBounds, DispatchPhase, &mut WindowContext) + 'static>;
 
 pub type ClickListener = Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>;
 
@@ -719,6 +721,18 @@ pub struct Interactivity {
     pub tooltip_builder: Option<TooltipBuilder>,
 }
 
+#[derive(Clone)]
+pub struct InteractiveBounds {
+    pub bounds: Bounds<Pixels>,
+    pub stacking_order: StackingOrder,
+}
+
+impl InteractiveBounds {
+    pub fn visibly_contains(&self, point: &Point<Pixels>, cx: &WindowContext) -> bool {
+        self.bounds.contains_point(point) && cx.was_top_layer(&point, &self.stacking_order)
+    }
+}
+
 impl Interactivity {
     pub fn layout(
         &mut self,
@@ -755,34 +769,52 @@ impl Interactivity {
     ) {
         let style = self.compute_style(Some(bounds), element_state, cx);
 
+        if style
+            .background
+            .as_ref()
+            .is_some_and(|fill| fill.color().is_some_and(|color| !color.is_transparent()))
+        {
+            cx.with_z_index(style.z_index.unwrap_or(0), |cx| cx.add_opaque_layer(bounds))
+        }
+
+        let interactive_bounds = Rc::new(InteractiveBounds {
+            bounds: bounds.intersect(&cx.content_mask().bounds),
+            stacking_order: cx.stacking_order().clone(),
+        });
+
         if let Some(mouse_cursor) = style.mouse_cursor {
-            let hovered = bounds.contains_point(&cx.mouse_position());
+            let mouse_position = &cx.mouse_position();
+            let hovered = interactive_bounds.visibly_contains(mouse_position, cx);
             if hovered {
                 cx.set_cursor_style(mouse_cursor);
             }
         }
 
         for listener in self.mouse_down_listeners.drain(..) {
+            let interactive_bounds = interactive_bounds.clone();
             cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
-                listener(event, &bounds, phase, cx);
+                listener(event, &*interactive_bounds, phase, cx);
             })
         }
 
         for listener in self.mouse_up_listeners.drain(..) {
+            let interactive_bounds = interactive_bounds.clone();
             cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| {
-                listener(event, &bounds, phase, cx);
+                listener(event, &*interactive_bounds, phase, cx);
             })
         }
 
         for listener in self.mouse_move_listeners.drain(..) {
+            let interactive_bounds = interactive_bounds.clone();
             cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
-                listener(event, &bounds, phase, cx);
+                listener(event, &*interactive_bounds, phase, cx);
             })
         }
 
         for listener in self.scroll_wheel_listeners.drain(..) {
+            let interactive_bounds = interactive_bounds.clone();
             cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| {
-                listener(event, &bounds, phase, cx);
+                listener(event, &*interactive_bounds, phase, cx);
             })
         }
 
@@ -792,6 +824,7 @@ impl Interactivity {
             .and_then(|group_hover| GroupBounds::get(&group_hover.group, cx));
 
         if let Some(group_bounds) = hover_group_bounds {
+            // todo!() needs cx.was_top_layer
             let hovered = group_bounds.contains_point(&cx.mouse_position());
             cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
                 if phase == DispatchPhase::Capture {
@@ -805,10 +838,11 @@ impl Interactivity {
         if self.hover_style.is_some()
             || (cx.active_drag.is_some() && !self.drag_over_styles.is_empty())
         {
-            let hovered = bounds.contains_point(&cx.mouse_position());
+            let interactive_bounds = interactive_bounds.clone();
+            let hovered = interactive_bounds.visibly_contains(&cx.mouse_position(), cx);
             cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
                 if phase == DispatchPhase::Capture {
-                    if bounds.contains_point(&event.position) != hovered {
+                    if interactive_bounds.visibly_contains(&event.position, cx) != hovered {
                         cx.notify();
                     }
                 }
@@ -817,8 +851,11 @@ impl Interactivity {
 
         if cx.active_drag.is_some() {
             let drop_listeners = mem::take(&mut self.drop_listeners);
+            let interactive_bounds = interactive_bounds.clone();
             cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| {
-                if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
+                if phase == DispatchPhase::Bubble
+                    && interactive_bounds.visibly_contains(&event.position, &cx)
+                {
                     if let Some(drag_state_type) =
                         cx.active_drag.as_ref().map(|drag| drag.view.entity_type())
                     {
@@ -847,6 +884,7 @@ impl Interactivity {
             if let Some(mouse_down) = mouse_down {
                 if let Some(drag_listener) = drag_listener {
                     let active_state = element_state.clicked_state.clone();
+                    let interactive_bounds = interactive_bounds.clone();
 
                     cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
                         if cx.active_drag.is_some() {
@@ -854,7 +892,7 @@ impl Interactivity {
                                 cx.notify();
                             }
                         } else if phase == DispatchPhase::Bubble
-                            && bounds.contains_point(&event.position)
+                            && interactive_bounds.visibly_contains(&event.position, cx)
                             && (event.position - mouse_down.position).magnitude() > DRAG_THRESHOLD
                         {
                             *active_state.borrow_mut() = ElementClickedState::default();
@@ -867,8 +905,11 @@ impl Interactivity {
                     });
                 }
 
+                let interactive_bounds = interactive_bounds.clone();
                 cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| {
-                    if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
+                    if phase == DispatchPhase::Bubble
+                        && interactive_bounds.visibly_contains(&event.position, cx)
+                    {
                         let mouse_click = ClickEvent {
                             down: mouse_down.clone(),
                             up: event.clone(),
@@ -881,8 +922,11 @@ impl Interactivity {
                     cx.notify();
                 });
             } else {
+                let interactive_bounds = interactive_bounds.clone();
                 cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
-                    if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
+                    if phase == DispatchPhase::Bubble
+                        && interactive_bounds.visibly_contains(&event.position, cx)
+                    {
                         *pending_mouse_down.borrow_mut() = Some(event.clone());
                         cx.notify();
                     }
@@ -893,13 +937,14 @@ impl Interactivity {
         if let Some(hover_listener) = self.hover_listener.take() {
             let was_hovered = element_state.hover_state.clone();
             let has_mouse_down = element_state.pending_mouse_down.clone();
+            let interactive_bounds = interactive_bounds.clone();
 
             cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
                 if phase != DispatchPhase::Bubble {
                     return;
                 }
-                let is_hovered =
-                    bounds.contains_point(&event.position) && has_mouse_down.borrow().is_none();
+                let is_hovered = interactive_bounds.visibly_contains(&event.position, cx)
+                    && has_mouse_down.borrow().is_none();
                 let mut was_hovered = was_hovered.borrow_mut();
 
                 if is_hovered != was_hovered.clone() {
@@ -914,14 +959,15 @@ impl Interactivity {
         if let Some(tooltip_builder) = self.tooltip_builder.take() {
             let active_tooltip = element_state.active_tooltip.clone();
             let pending_mouse_down = element_state.pending_mouse_down.clone();
+            let interactive_bounds = interactive_bounds.clone();
 
             cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
                 if phase != DispatchPhase::Bubble {
                     return;
                 }
 
-                let is_hovered =
-                    bounds.contains_point(&event.position) && pending_mouse_down.borrow().is_none();
+                let is_hovered = interactive_bounds.visibly_contains(&event.position, cx)
+                    && pending_mouse_down.borrow().is_none();
                 if !is_hovered {
                     active_tooltip.borrow_mut().take();
                     return;
@@ -979,11 +1025,12 @@ impl Interactivity {
                 .group_active_style
                 .as_ref()
                 .and_then(|group_active| GroupBounds::get(&group_active.group, cx));
+            let interactive_bounds = interactive_bounds.clone();
             cx.on_mouse_event(move |down: &MouseDownEvent, phase, cx| {
                 if phase == DispatchPhase::Bubble {
                     let group = active_group_bounds
                         .map_or(false, |bounds| bounds.contains_point(&down.position));
-                    let element = bounds.contains_point(&down.position);
+                    let element = interactive_bounds.visibly_contains(&down.position, cx);
                     if group || element {
                         *active_state.borrow_mut() = ElementClickedState { group, element };
                         cx.notify();
@@ -1000,9 +1047,12 @@ impl Interactivity {
                 .clone();
             let line_height = cx.line_height();
             let scroll_max = (content_size - bounds.size).max(&Size::default());
+            let interactive_bounds = interactive_bounds.clone();
 
             cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| {
-                if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
+                if phase == DispatchPhase::Bubble
+                    && interactive_bounds.visibly_contains(&event.position, cx)
+                {
                     let mut scroll_offset = scroll_offset.borrow_mut();
                     let old_scroll_offset = *scroll_offset;
                     let delta = event.delta.pixel_delta(line_height);
@@ -1098,19 +1148,21 @@ impl Interactivity {
                     }
                 }
             }
-            // if self.hover_style.is_some() {
-            if bounds.contains_point(&mouse_position) {
-                // eprintln!("div hovered {bounds:?} {mouse_position:?}");
-                style.refine(&self.hover_style);
-            } else {
-                // eprintln!("div NOT hovered {bounds:?} {mouse_position:?}");
+            if self.hover_style.is_some() {
+                if bounds
+                    .intersect(&cx.content_mask().bounds)
+                    .contains_point(&mouse_position)
+                    && cx.was_top_layer(&mouse_position, cx.stacking_order())
+                {
+                    style.refine(&self.hover_style);
+                }
             }
-            // }
 
             if let Some(drag) = cx.active_drag.take() {
                 for (state_type, group_drag_style) in &self.group_drag_over_styles {
                     if let Some(group_bounds) = GroupBounds::get(&group_drag_style.group, cx) {
                         if *state_type == drag.view.entity_type()
+                        // todo!() needs to handle cx.content_mask() and cx.is_top()
                             && group_bounds.contains_point(&mouse_position)
                         {
                             style.refine(&group_drag_style.style);
@@ -1120,7 +1172,10 @@ impl Interactivity {
 
                 for (state_type, drag_over_style) in &self.drag_over_styles {
                     if *state_type == drag.view.entity_type()
-                        && bounds.contains_point(&mouse_position)
+                        && bounds
+                            .intersect(&cx.content_mask().bounds)
+                            .contains_point(&mouse_position)
+                        && cx.was_top_layer(&mouse_position, cx.stacking_order())
                     {
                         style.refine(drag_over_style);
                     }

crates/gpui2/src/window.rs 🔗

@@ -39,8 +39,8 @@ use util::ResultExt;
 
 /// A global stacking order, which is created by stacking successive z-index values.
 /// Each z-index will always be interpreted in the context of its parent z-index.
-#[derive(Deref, DerefMut, Ord, PartialOrd, Eq, PartialEq, Clone, Default)]
-pub(crate) struct StackingOrder(pub(crate) SmallVec<[u32; 16]>);
+#[derive(Deref, DerefMut, Ord, PartialOrd, Eq, PartialEq, Clone, Default, Debug)]
+pub struct StackingOrder(pub(crate) SmallVec<[u32; 16]>);
 
 /// Represents the two different phases when dispatching events.
 #[derive(Default, Copy, Clone, Debug, Eq, PartialEq)]
@@ -243,7 +243,8 @@ pub(crate) struct Frame {
     pub(crate) dispatch_tree: DispatchTree,
     pub(crate) focus_listeners: Vec<AnyFocusListener>,
     pub(crate) scene_builder: SceneBuilder,
-    z_index_stack: StackingOrder,
+    pub(crate) depth_map: Vec<(StackingOrder, Bounds<Pixels>)>,
+    pub(crate) z_index_stack: StackingOrder,
     content_mask_stack: Vec<ContentMask<Pixels>>,
     element_offset_stack: Vec<Point<Pixels>>,
 }
@@ -257,6 +258,7 @@ impl Frame {
             focus_listeners: Vec::new(),
             scene_builder: SceneBuilder::default(),
             z_index_stack: StackingOrder::default(),
+            depth_map: Default::default(),
             content_mask_stack: Vec::new(),
             element_offset_stack: Vec::new(),
         }
@@ -806,6 +808,32 @@ impl<'a> WindowContext<'a> {
         result
     }
 
+    /// Called during painting to track which z-index is on top at each pixel position
+    pub fn add_opaque_layer(&mut self, bounds: Bounds<Pixels>) {
+        let stacking_order = self.window.current_frame.z_index_stack.clone();
+        let depth_map = &mut self.window.current_frame.depth_map;
+        match depth_map.binary_search_by(|(level, _)| stacking_order.cmp(&level)) {
+            Ok(i) | Err(i) => depth_map.insert(i, (stacking_order, bounds)),
+        }
+    }
+
+    /// Returns true if the top-most opaque layer painted over this point was part of the
+    /// same layer as the given stacking order.
+    pub fn was_top_layer(&self, point: &Point<Pixels>, level: &StackingOrder) -> bool {
+        for (stack, bounds) in self.window.previous_frame.depth_map.iter() {
+            if bounds.contains_point(point) {
+                return level.starts_with(stack) || stack.starts_with(level);
+            }
+        }
+
+        false
+    }
+
+    /// Called during painting to get the current stacking order.
+    pub fn stacking_order(&self) -> &StackingOrder {
+        &self.window.current_frame.z_index_stack
+    }
+
     /// Paint one or more drop shadows into the scene for the current frame at the current z-index.
     pub fn paint_shadows(
         &mut self,
@@ -1153,6 +1181,7 @@ impl<'a> WindowContext<'a> {
         frame.mouse_listeners.values_mut().for_each(Vec::clear);
         frame.focus_listeners.clear();
         frame.dispatch_tree.clear();
+        frame.depth_map.clear();
     }
 
     /// Dispatch a mouse or keyboard event on the window.

crates/node_runtime/src/node_runtime.rs 🔗

@@ -73,6 +73,7 @@ impl RealNodeRuntime {
         let npm_file = node_dir.join("bin/npm");
 
         let result = Command::new(&node_binary)
+            .env_clear()
             .arg(npm_file)
             .arg("--version")
             .stdin(Stdio::null())
@@ -149,6 +150,7 @@ impl NodeRuntime for RealNodeRuntime {
             }
 
             let mut command = Command::new(node_binary);
+            command.env_clear();
             command.env("PATH", env_path);
             command.arg(npm_file).arg(subcommand);
             command.args(["--cache".into(), installation_path.join("cache")]);
@@ -200,11 +202,11 @@ impl NodeRuntime for RealNodeRuntime {
                 &[
                     name,
                     "--json",
-                    "-fetch-retry-mintimeout",
+                    "--fetch-retry-mintimeout",
                     "2000",
-                    "-fetch-retry-maxtimeout",
+                    "--fetch-retry-maxtimeout",
                     "5000",
-                    "-fetch-timeout",
+                    "--fetch-timeout",
                     "5000",
                 ],
             )
@@ -229,11 +231,11 @@ impl NodeRuntime for RealNodeRuntime {
 
         let mut arguments: Vec<_> = packages.iter().map(|p| p.as_str()).collect();
         arguments.extend_from_slice(&[
-            "-fetch-retry-mintimeout",
+            "--fetch-retry-mintimeout",
             "2000",
-            "-fetch-retry-maxtimeout",
+            "--fetch-retry-maxtimeout",
             "5000",
-            "-fetch-timeout",
+            "--fetch-timeout",
             "5000",
         ]);
 

crates/picker2/src/picker2.rs 🔗

@@ -16,6 +16,7 @@ pub struct Picker<D: PickerDelegate> {
 }
 
 pub trait PickerDelegate: Sized + 'static {
+    type ListItem: IntoElement;
     fn match_count(&self) -> usize;
     fn selected_index(&self) -> usize;
     fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>);
@@ -31,7 +32,7 @@ pub trait PickerDelegate: Sized + 'static {
         ix: usize,
         selected: bool,
         cx: &mut ViewContext<Picker<Self>>,
-    ) -> AnyElement;
+    ) -> Option<Self::ListItem>;
 }
 
 impl<D: PickerDelegate> FocusableView for Picker<D> {
@@ -113,7 +114,6 @@ impl<D: PickerDelegate> Picker<D> {
     }
 
     fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
-        dbg!("canceling!");
         self.delegate.dismissed(cx);
     }
 
@@ -229,7 +229,7 @@ impl<D: PickerDelegate> Render for Picker<D> {
                                                             )
                                                         }),
                                                     )
-                                                    .child(picker.delegate.render_match(
+                                                    .children(picker.delegate.render_match(
                                                         ix,
                                                         ix == selected_index,
                                                         cx,

crates/project_panel2/src/project_panel.rs 🔗

@@ -10,9 +10,8 @@ use anyhow::{anyhow, Result};
 use gpui::{
     actions, div, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext,
     ClipboardItem, Div, EventEmitter, FocusHandle, Focusable, FocusableView, InteractiveElement,
-    IntoElement, Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel,
-    Render, Stateful, StatefulInteractiveElement, Styled, Task, UniformListScrollHandle, View,
-    ViewContext, VisualContext as _, WeakView, WindowContext,
+    Model, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, Stateful, Styled,
+    Task, UniformListScrollHandle, View, ViewContext, VisualContext as _, WeakView, WindowContext,
 };
 use menu::{Confirm, SelectNext, SelectPrev};
 use project::{
@@ -30,7 +29,7 @@ use std::{
     sync::Arc,
 };
 use theme::ActiveTheme as _;
-use ui::{h_stack, v_stack, IconElement, Label};
+use ui::{v_stack, IconElement, Label, ListItem};
 use unicase::UniCase;
 use util::{maybe, ResultExt, TryFutureExt};
 use workspace::{
@@ -1335,13 +1334,19 @@ impl ProjectPanel {
         }
     }
 
-    fn render_entry_visual_element(
-        details: &EntryDetails,
-        editor: Option<&View<Editor>>,
-        padding: Pixels,
+    fn render_entry(
+        &self,
+        entry_id: ProjectEntryId,
+        details: EntryDetails,
+        // dragged_entry_destination: &mut Option<Arc<Path>>,
         cx: &mut ViewContext<Self>,
-    ) -> Div {
+    ) -> ListItem {
+        let kind = details.kind;
+        let settings = ProjectPanelSettings::get_global(cx);
         let show_editor = details.is_editing && !details.is_processing;
+        let is_selected = self
+            .selection
+            .map_or(false, |selection| selection.entry_id == entry_id);
 
         let theme = cx.theme();
         let filename_text_color = details
@@ -1354,14 +1359,17 @@ impl ProjectPanel {
             })
             .unwrap_or(theme.status().info);
 
-        h_stack()
+        ListItem::new(entry_id.to_proto() as usize)
+            .indent_level(details.depth)
+            .indent_step_size(px(settings.indent_size))
+            .selected(is_selected)
             .child(if let Some(icon) = &details.icon {
                 div().child(IconElement::from_path(icon.to_string()))
             } else {
                 div()
             })
             .child(
-                if let (Some(editor), true) = (editor, show_editor) {
+                if let (Some(editor), true) = (Some(&self.filename_editor), show_editor) {
                     div().w_full().child(editor.clone())
                 } else {
                     div()
@@ -1370,33 +1378,6 @@ impl ProjectPanel {
                 }
                 .ml_1(),
             )
-            .pl(padding)
-    }
-
-    fn render_entry(
-        &self,
-        entry_id: ProjectEntryId,
-        details: EntryDetails,
-        // dragged_entry_destination: &mut Option<Arc<Path>>,
-        cx: &mut ViewContext<Self>,
-    ) -> Stateful<Div> {
-        let kind = details.kind;
-        let settings = ProjectPanelSettings::get_global(cx);
-        const INDENT_SIZE: Pixels = px(16.0);
-        let padding = INDENT_SIZE + details.depth as f32 * px(settings.indent_size);
-        let show_editor = details.is_editing && !details.is_processing;
-        let is_selected = self
-            .selection
-            .map_or(false, |selection| selection.entry_id == entry_id);
-
-        Self::render_entry_visual_element(&details, Some(&self.filename_editor), padding, cx)
-            .id(entry_id.to_proto() as usize)
-            .w_full()
-            .cursor_pointer()
-            .when(is_selected, |this| {
-                this.bg(cx.theme().colors().element_selected)
-            })
-            .hover(|style| style.bg(cx.theme().colors().element_hover))
             .on_click(cx.listener(move |this, event: &gpui::ClickEvent, cx| {
                 if !show_editor {
                     if kind.is_dir() {
@@ -1410,12 +1391,9 @@ impl ProjectPanel {
                     }
                 }
             }))
-            .on_mouse_down(
-                MouseButton::Right,
-                cx.listener(move |this, event: &MouseDownEvent, cx| {
-                    this.deploy_context_menu(event.position, entry_id, cx);
-                }),
-            )
+            .on_secondary_mouse_down(cx.listener(move |this, event: &MouseDownEvent, cx| {
+                this.deploy_context_menu(event.position, entry_id, cx);
+            }))
         // .on_drop::<ProjectEntryId>(|this, event, cx| {
         //     this.move_entry(
         //         *dragged_entry,

crates/storybook2/src/stories/focus.rs 🔗

@@ -33,7 +33,6 @@ impl Render for FocusStory {
         let theme = cx.theme();
         let color_1 = theme.status().created;
         let color_2 = theme.status().modified;
-        let color_3 = theme.status().deleted;
         let color_4 = theme.status().conflict;
         let color_5 = theme.status().ignored;
         let color_6 = theme.status().renamed;
@@ -42,10 +41,10 @@ impl Render for FocusStory {
             .id("parent")
             .focusable()
             .key_context("parent")
-            .on_action(cx.listener(|_, action: &ActionA, cx| {
+            .on_action(cx.listener(|_, _action: &ActionA, _cx| {
                 println!("Action A dispatched on parent");
             }))
-            .on_action(cx.listener(|_, action: &ActionB, cx| {
+            .on_action(cx.listener(|_, _action: &ActionB, _cx| {
                 println!("Action B dispatched on parent");
             }))
             .on_focus(cx.listener(|_, _, _| println!("Parent focused")))
@@ -61,7 +60,7 @@ impl Render for FocusStory {
                 div()
                     .track_focus(&self.child_1_focus)
                     .key_context("child-1")
-                    .on_action(cx.listener(|_, action: &ActionB, cx| {
+                    .on_action(cx.listener(|_, _action: &ActionB, _cx| {
                         println!("Action B dispatched on child 1 during");
                     }))
                     .w_full()
@@ -83,7 +82,7 @@ impl Render for FocusStory {
                 div()
                     .track_focus(&self.child_2_focus)
                     .key_context("child-2")
-                    .on_action(cx.listener(|_, action: &ActionC, cx| {
+                    .on_action(cx.listener(|_, _action: &ActionC, _cx| {
                         println!("Action C dispatched on child 2");
                     }))
                     .w_full()

crates/storybook2/src/stories/picker.rs 🔗

@@ -1,11 +1,11 @@
 use fuzzy::StringMatchCandidate;
 use gpui::{
-    div, prelude::*, AnyElement, Div, KeyBinding, Render, SharedString, Styled, Task, View,
-    WindowContext,
+    div, prelude::*, Div, KeyBinding, Render, SharedString, Styled, Task, View, WindowContext,
 };
 use picker::{Picker, PickerDelegate};
 use std::sync::Arc;
 use theme2::ActiveTheme;
+use ui::{Label, ListItem};
 
 pub struct PickerStory {
     picker: View<Picker<Delegate>>,
@@ -37,6 +37,8 @@ impl Delegate {
 }
 
 impl PickerDelegate for Delegate {
+    type ListItem = ListItem;
+
     fn match_count(&self) -> usize {
         self.candidates.len()
     }
@@ -49,27 +51,20 @@ impl PickerDelegate for Delegate {
         &self,
         ix: usize,
         selected: bool,
-        cx: &mut gpui::ViewContext<Picker<Self>>,
-    ) -> AnyElement {
-        let colors = cx.theme().colors();
+        _cx: &mut gpui::ViewContext<Picker<Self>>,
+    ) -> Option<Self::ListItem> {
         let Some(candidate_ix) = self.matches.get(ix) else {
-            return div().into_any();
+            return None;
         };
         // TASK: Make StringMatchCandidate::string a SharedString
         let candidate = SharedString::from(self.candidates[*candidate_ix].string.clone());
 
-        div()
-            .text_color(colors.text)
-            .when(selected, |s| {
-                s.border_l_10().border_color(colors.terminal_ansi_yellow)
-            })
-            .hover(|style| {
-                style
-                    .bg(colors.element_active)
-                    .text_color(colors.text_accent)
-            })
-            .child(candidate)
-            .into_any()
+        Some(
+            ListItem::new(ix)
+                .inset(true)
+                .selected(selected)
+                .child(Label::new(candidate)),
+        )
     }
 
     fn selected_index(&self) -> usize {
@@ -81,7 +76,7 @@ impl PickerDelegate for Delegate {
         cx.notify();
     }
 
-    fn confirm(&mut self, secondary: bool, cx: &mut gpui::ViewContext<Picker<Self>>) {
+    fn confirm(&mut self, secondary: bool, _cx: &mut gpui::ViewContext<Picker<Self>>) {
         let candidate_ix = self.matches[self.selected_ix];
         let candidate = self.candidates[candidate_ix].string.clone();
 

crates/storybook2/src/stories/scroll.rs 🔗

@@ -6,7 +6,7 @@ pub struct ScrollStory;
 
 impl ScrollStory {
     pub fn view(cx: &mut WindowContext) -> View<ScrollStory> {
-        cx.build_view(|cx| ScrollStory)
+        cx.build_view(|_cx| ScrollStory)
     }
 }
 

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

@@ -8,7 +8,7 @@ pub struct TextStory;
 
 impl TextStory {
     pub fn view(cx: &mut WindowContext) -> View<Self> {
-        cx.build_view(|cx| Self)
+        cx.build_view(|_cx| Self)
     }
 }
 
@@ -68,7 +68,7 @@ impl Render for TextStory {
                         cx.text_style().to_run(18),
                     ]),
                 )
-                .on_click(vec![2..4, 1..3, 7..9], |range_ix, cx| {
+                .on_click(vec![2..4, 1..3, 7..9], |range_ix, _cx| {
                     println!("Clicked range {range_ix}");
                 })
             )

crates/storybook2/src/stories/z_index.rs 🔗

@@ -9,7 +9,7 @@ pub struct ZIndexStory;
 impl Render for ZIndexStory {
     type Element = Div;
 
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
         Story::container().child(Story::title("z-index")).child(
             div()
                 .flex()
@@ -84,7 +84,7 @@ struct ZIndexExample {
 impl RenderOnce for ZIndexExample {
     type Rendered = Div;
 
-    fn render(self, cx: &mut WindowContext) -> Self::Rendered {
+    fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
         div()
             .relative()
             .size_full()

crates/storybook2/src/story_selector.rs 🔗

@@ -18,6 +18,7 @@ pub enum ComponentStory {
     ContextMenu,
     Focus,
     Icon,
+    IconButton,
     Input,
     Keybinding,
     Label,
@@ -37,6 +38,7 @@ impl ComponentStory {
             Self::ContextMenu => cx.build_view(|_| ui::ContextMenuStory).into(),
             Self::Focus => FocusStory::view(cx).into(),
             Self::Icon => cx.build_view(|_| ui::IconStory).into(),
+            Self::IconButton => cx.build_view(|_| ui::IconButtonStory).into(),
             Self::Input => cx.build_view(|_| ui::InputStory).into(),
             Self::Keybinding => cx.build_view(|_| ui::KeybindingStory).into(),
             Self::Label => cx.build_view(|_| ui::LabelStory).into(),

crates/storybook2/src/storybook2.rs 🔗

@@ -1,5 +1,3 @@
-#![allow(dead_code, unused_variables)]
-
 mod assets;
 mod stories;
 mod story_selector;
@@ -70,7 +68,7 @@ fn main() {
         language::init(cx);
         editor::init(cx);
 
-        let window = cx.open_window(
+        let _window = cx.open_window(
             WindowOptions {
                 bounds: WindowBounds::Fixed(Bounds {
                     origin: Default::default(),
@@ -104,7 +102,7 @@ impl StoryWrapper {
 impl Render for StoryWrapper {
     type Element = Div;
 
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
         div()
             .flex()
             .flex_col()

crates/theme2/src/default_colors.rs 🔗

@@ -23,15 +23,15 @@ impl ThemeColors {
             surface_background: neutral().light().step_2(),
             background: neutral().light().step_1(),
             element_background: neutral().light().step_3(),
-            element_hover: neutral().light().step_4(),
-            element_active: neutral().light().step_5(),
-            element_selected: neutral().light().step_5(),
+            element_hover: neutral().light_alpha().step_4(),
+            element_active: neutral().light_alpha().step_5(),
+            element_selected: neutral().light_alpha().step_5(),
             element_disabled: neutral().light_alpha().step_3(),
             drop_target_background: blue().light_alpha().step_2(),
             ghost_element_background: system.transparent,
-            ghost_element_hover: neutral().light().step_4(),
-            ghost_element_active: neutral().light().step_5(),
-            ghost_element_selected: neutral().light().step_5(),
+            ghost_element_hover: neutral().light_alpha().step_4(),
+            ghost_element_active: neutral().light_alpha().step_5(),
+            ghost_element_selected: neutral().light_alpha().step_5(),
             ghost_element_disabled: neutral().light_alpha().step_3(),
             text: yellow().light().step_9(),
             text_muted: neutral().light().step_11(),
@@ -95,15 +95,15 @@ impl ThemeColors {
             surface_background: neutral().dark().step_2(),
             background: neutral().dark().step_1(),
             element_background: neutral().dark().step_3(),
-            element_hover: neutral().dark().step_4(),
-            element_active: neutral().dark().step_5(),
-            element_selected: neutral().dark().step_5(),
+            element_hover: neutral().dark_alpha().step_4(),
+            element_active: neutral().dark_alpha().step_5(),
+            element_selected: neutral().dark_alpha().step_5(),
             element_disabled: neutral().dark_alpha().step_3(),
             drop_target_background: blue().dark_alpha().step_2(),
             ghost_element_background: system.transparent,
-            ghost_element_hover: neutral().dark().step_4(),
-            ghost_element_active: neutral().dark().step_5(),
-            ghost_element_selected: neutral().dark().step_5(),
+            ghost_element_hover: neutral().dark_alpha().step_4(),
+            ghost_element_active: neutral().dark_alpha().step_5(),
+            ghost_element_selected: neutral().dark_alpha().step_5(),
             ghost_element_disabled: neutral().dark_alpha().step_3(),
             text: neutral().dark().step_12(),
             text_muted: neutral().dark().step_11(),

crates/theme2/src/one_themes.rs 🔗

@@ -20,7 +20,7 @@ pub fn one_family() -> ThemeFamily {
 pub(crate) fn one_dark() -> Theme {
     let bg = hsla(215. / 360., 12. / 100., 15. / 100., 1.);
     let editor = hsla(220. / 360., 12. / 100., 18. / 100., 1.);
-    let elevated_surface = hsla(220. / 360., 12. / 100., 18. / 100., 1.);
+    let elevated_surface = hsla(225. / 360., 12. / 100., 17. / 100., 1.);
 
     let blue = hsla(207.8 / 360., 81. / 100., 66. / 100., 1.0);
     let gray = hsla(218.8 / 360., 10. / 100., 40. / 100., 1.0);
@@ -48,7 +48,7 @@ pub(crate) fn one_dark() -> Theme {
                 elevated_surface_background: elevated_surface,
                 surface_background: bg,
                 background: bg,
-                element_background: elevated_surface,
+                element_background: hsla(223.0 / 360., 13. / 100., 21. / 100., 1.0),
                 element_hover: hsla(225.0 / 360., 11.8 / 100., 26.7 / 100., 1.0),
                 element_active: hsla(220.0 / 360., 11.8 / 100., 20.0 / 100., 1.0),
                 element_selected: hsla(224.0 / 360., 11.3 / 100., 26.1 / 100., 1.0),

crates/theme_selector2/Cargo.toml 🔗

@@ -13,6 +13,7 @@ editor = { package = "editor2", path = "../editor2" }
 fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
 fs = { package = "fs2", path = "../fs2" }
 gpui = { package = "gpui2", path = "../gpui2" }
+ui = { package = "ui2", path = "../ui2" }
 picker = { package = "picker2", path = "../picker2" }
 theme = { package = "theme2", path = "../theme2" }
 settings = { package = "settings2", path = "../settings2" }

crates/theme_selector2/src/theme_selector.rs 🔗

@@ -2,19 +2,16 @@ use feature_flags::FeatureFlagAppExt;
 use fs::Fs;
 use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
 use gpui::{
-    actions, div, AnyElement, AppContext, DismissEvent, Element, EventEmitter, FocusableView,
-    InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, View,
-    ViewContext, VisualContext, WeakView,
+    actions, AppContext, DismissEvent, EventEmitter, FocusableView, ParentElement, Render,
+    SharedString, View, ViewContext, VisualContext, WeakView,
 };
 use picker::{Picker, PickerDelegate};
 use settings::{update_settings_file, SettingsStore};
 use std::sync::Arc;
 use theme::{ActiveTheme, Theme, ThemeRegistry, ThemeSettings};
+use ui::ListItem;
 use util::ResultExt;
-use workspace::{
-    ui::{HighlightedLabel, StyledExt},
-    Workspace,
-};
+use workspace::{ui::HighlightedLabel, Workspace};
 
 actions!(Toggle, Reload);
 
@@ -160,6 +157,8 @@ impl ThemeSelectorDelegate {
 }
 
 impl PickerDelegate for ThemeSelectorDelegate {
+    type ListItem = ui::ListItem;
+
     fn placeholder_text(&self) -> Arc<str> {
         "Select Theme...".into()
     }
@@ -260,24 +259,18 @@ impl PickerDelegate for ThemeSelectorDelegate {
         &self,
         ix: usize,
         selected: bool,
-        cx: &mut ViewContext<Picker<Self>>,
-    ) -> AnyElement {
-        let theme = cx.theme();
-        let colors = theme.colors();
-
+        _cx: &mut ViewContext<Picker<Self>>,
+    ) -> Option<Self::ListItem> {
         let theme_match = &self.matches[ix];
-        div()
-            .px_1()
-            .text_color(colors.text)
-            .text_ui()
-            .bg(colors.ghost_element_background)
-            .rounded_md()
-            .when(selected, |this| this.bg(colors.ghost_element_selected))
-            .hover(|this| this.bg(colors.ghost_element_hover))
-            .child(HighlightedLabel::new(
-                theme_match.string.clone(),
-                theme_match.positions.clone(),
-            ))
-            .into_any()
+
+        Some(
+            ListItem::new(ix)
+                .inset(true)
+                .selected(selected)
+                .child(HighlightedLabel::new(
+                    theme_match.string.clone(),
+                    theme_match.positions.clone(),
+                )),
+        )
     }
 }

crates/ui2/Cargo.toml 🔗

@@ -15,11 +15,11 @@ gpui = { package = "gpui2", path = "../gpui2" }
 itertools = { version = "0.11.0", optional = true }
 menu = { package = "menu2", path = "../menu2"}
 serde.workspace = true
-settings2 = { path = "../settings2" }
+settings = { package = "settings2", path = "../settings2" }
 smallvec.workspace = true
 story = { path = "../story", optional = true }
 strum = { version = "0.25.0", features = ["derive"] }
-theme2 = { path = "../theme2" }
+theme = { package = "theme2", path = "../theme2" }
 rand = "0.8"
 
 [features]

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

@@ -49,6 +49,12 @@ impl Avatar {
         }
     }
 
+    pub fn source(src: ImageSource) -> Self {
+        Self {
+            src,
+            shape: Shape::Circle,
+        }
+    }
     pub fn shape(mut self, shape: Shape) -> Self {
         self.shape = shape;
         self

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

@@ -1,7 +1,6 @@
 use gpui::{div, prelude::*, Div, Element, ElementId, IntoElement, Styled, WindowContext};
 
-use theme2::ActiveTheme;
-
+use crate::prelude::*;
 use crate::{Color, Icon, IconElement, Selection};
 
 pub type CheckHandler = Box<dyn Fn(&Selection, &mut WindowContext) + 'static>;

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

@@ -264,7 +264,7 @@ impl<M: ManagedView> Element for MenuHandle<M> {
 
                 let new_menu = (builder)(cx);
                 let menu2 = menu.clone();
-                cx.subscribe(&new_menu, move |modal, e, cx| match e {
+                cx.subscribe(&new_menu, move |_modal, e, cx| match e {
                     &DismissEvent::Dismiss => {
                         *menu2.borrow_mut() = None;
                         cx.notify();

crates/ui2/src/components/divider.rs 🔗

@@ -49,17 +49,4 @@ impl Divider {
         self.inset = true;
         self
     }
-
-    fn render(self, cx: &mut WindowContext) -> impl Element {
-        div()
-            .map(|this| match self.direction {
-                DividerDirection::Horizontal => {
-                    this.h_px().w_full().when(self.inset, |this| this.mx_1p5())
-                }
-                DividerDirection::Vertical => {
-                    this.w_px().h_full().when(self.inset, |this| this.my_1p5())
-                }
-            })
-            .bg(cx.theme().colors().border_variant)
-    }
 }

crates/ui2/src/components/icon.rs 🔗

@@ -14,6 +14,8 @@ pub enum IconSize {
 pub enum Icon {
     Ai,
     ArrowLeft,
+    ArrowUp,
+    ArrowDown,
     ArrowRight,
     ArrowUpRight,
     AtSign,
@@ -61,6 +63,7 @@ pub enum Icon {
     Mic,
     MicMute,
     Plus,
+    Public,
     Quote,
     Replace,
     ReplaceAll,
@@ -71,6 +74,11 @@ pub enum Icon {
     Terminal,
     WholeWord,
     XCircle,
+    Command,
+    Control,
+    Shift,
+    Option,
+    Return,
 }
 
 impl Icon {
@@ -79,6 +87,8 @@ impl Icon {
             Icon::Ai => "icons/ai.svg",
             Icon::ArrowLeft => "icons/arrow_left.svg",
             Icon::ArrowRight => "icons/arrow_right.svg",
+            Icon::ArrowUp => "icons/arrow_up.svg",
+            Icon::ArrowDown => "icons/arrow_down.svg",
             Icon::ArrowUpRight => "icons/arrow_up_right.svg",
             Icon::AtSign => "icons/at-sign.svg",
             Icon::AudioOff => "icons/speaker-off.svg",
@@ -125,6 +135,7 @@ impl Icon {
             Icon::Mic => "icons/mic.svg",
             Icon::MicMute => "icons/mic-mute.svg",
             Icon::Plus => "icons/plus.svg",
+            Icon::Public => "icons/public.svg",
             Icon::Quote => "icons/quote.svg",
             Icon::Replace => "icons/replace.svg",
             Icon::ReplaceAll => "icons/replace_all.svg",
@@ -135,6 +146,11 @@ impl Icon {
             Icon::Terminal => "icons/terminal.svg",
             Icon::WholeWord => "icons/word_search.svg",
             Icon::XCircle => "icons/error.svg",
+            Icon::Command => "icons/command.svg",
+            Icon::Control => "icons/control.svg",
+            Icon::Shift => "icons/shift.svg",
+            Icon::Option => "icons/option.svg",
+            Icon::Return => "icons/return.svg",
         }
     }
 }
@@ -151,8 +167,8 @@ impl RenderOnce for IconElement {
 
     fn render(self, cx: &mut WindowContext) -> Self::Rendered {
         let svg_size = match self.size {
-            IconSize::Small => rems(0.75),
-            IconSize::Medium => rems(0.9375),
+            IconSize::Small => rems(14. / 16.),
+            IconSize::Medium => rems(16. / 16.),
         };
 
         svg()
@@ -189,17 +205,4 @@ impl IconElement {
         self.size = size;
         self
     }
-
-    fn render(self, cx: &mut WindowContext) -> impl Element {
-        let svg_size = match self.size {
-            IconSize::Small => rems(0.75),
-            IconSize::Medium => rems(0.9375),
-        };
-
-        svg()
-            .size(svg_size)
-            .flex_none()
-            .path(self.path)
-            .text_color(self.color.color(cx))
-    }
 }

crates/ui2/src/components/icon_button.rs 🔗

@@ -23,15 +23,13 @@ impl RenderOnce for IconButton {
             _ => self.color,
         };
 
-        let (mut bg_color, bg_hover_color, bg_active_color) = match self.variant {
+        let (mut bg_color, bg_active_color) = match self.variant {
             ButtonVariant::Filled => (
                 cx.theme().colors().element_background,
-                cx.theme().colors().element_hover,
                 cx.theme().colors().element_active,
             ),
             ButtonVariant::Ghost => (
                 cx.theme().colors().ghost_element_background,
-                cx.theme().colors().ghost_element_hover,
                 cx.theme().colors().ghost_element_active,
             ),
         };
@@ -67,7 +65,8 @@ impl RenderOnce for IconButton {
             }
         }
 
-        button
+        // HACK: Add an additional identified element wrapper to fix tooltips not showing up.
+        div().id(self.id.clone()).child(button)
     }
 }
 
@@ -124,6 +123,6 @@ impl IconButton {
     }
 
     pub fn action(self, action: Box<dyn Action>) -> Self {
-        self.on_click(move |this, cx| cx.dispatch_action(action.boxed_clone()))
+        self.on_click(move |_event, cx| cx.dispatch_action(action.boxed_clone()))
     }
 }

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

@@ -1,5 +1,5 @@
-use crate::prelude::*;
-use gpui::{Action, Div, IntoElement};
+use crate::{h_stack, prelude::*, Icon, IconElement, IconSize};
+use gpui::{relative, rems, Action, Div, IntoElement, Keystroke};
 
 #[derive(IntoElement, Clone)]
 pub struct KeyBinding {
@@ -14,19 +14,35 @@ impl RenderOnce for KeyBinding {
     type Rendered = Div;
 
     fn render(self, cx: &mut WindowContext) -> Self::Rendered {
-        div()
-            .flex()
+        h_stack()
+            .flex_none()
             .gap_2()
             .children(self.key_binding.keystrokes().iter().map(|keystroke| {
-                div()
-                    .flex()
-                    .gap_1()
+                let key_icon = Self::icon_for_key(&keystroke);
+
+                h_stack()
+                    .flex_none()
+                    .gap_0p5()
+                    .bg(cx.theme().colors().element_background)
+                    .p_0p5()
+                    .rounded_sm()
                     .when(keystroke.modifiers.function, |el| el.child(Key::new("fn")))
-                    .when(keystroke.modifiers.control, |el| el.child(Key::new("^")))
-                    .when(keystroke.modifiers.alt, |el| el.child(Key::new("⌥")))
-                    .when(keystroke.modifiers.command, |el| el.child(Key::new("⌘")))
-                    .when(keystroke.modifiers.shift, |el| el.child(Key::new("⇧")))
-                    .child(Key::new(keystroke.key.clone()))
+                    .when(keystroke.modifiers.control, |el| {
+                        el.child(KeyIcon::new(Icon::Control))
+                    })
+                    .when(keystroke.modifiers.alt, |el| {
+                        el.child(KeyIcon::new(Icon::Option))
+                    })
+                    .when(keystroke.modifiers.command, |el| {
+                        el.child(KeyIcon::new(Icon::Command))
+                    })
+                    .when(keystroke.modifiers.shift, |el| {
+                        el.child(KeyIcon::new(Icon::Shift))
+                    })
+                    .when_some(key_icon, |el, icon| el.child(KeyIcon::new(icon)))
+                    .when(key_icon.is_none(), |el| {
+                        el.child(Key::new(keystroke.key.to_uppercase().clone()))
+                    })
             }))
     }
 }
@@ -39,6 +55,22 @@ impl KeyBinding {
         Some(Self::new(key_binding))
     }
 
+    fn icon_for_key(keystroke: &Keystroke) -> Option<Icon> {
+        let mut icon: Option<Icon> = None;
+
+        if keystroke.key == "left".to_string() {
+            icon = Some(Icon::ArrowLeft);
+        } else if keystroke.key == "right".to_string() {
+            icon = Some(Icon::ArrowRight);
+        } else if keystroke.key == "up".to_string() {
+            icon = Some(Icon::ArrowUp);
+        } else if keystroke.key == "down".to_string() {
+            icon = Some(Icon::ArrowDown);
+        }
+
+        icon
+    }
+
     pub fn new(key_binding: gpui::KeyBinding) -> Self {
         Self { key_binding }
     }
@@ -53,13 +85,18 @@ impl RenderOnce for Key {
     type Rendered = Div;
 
     fn render(self, cx: &mut WindowContext) -> Self::Rendered {
+        let single_char = self.key.len() == 1;
+
         div()
-            .px_2()
             .py_0()
-            .rounded_md()
-            .text_ui_sm()
+            .when(single_char, |el| {
+                el.w(rems(14. / 16.)).flex().flex_none().justify_center()
+            })
+            .when(!single_char, |el| el.px_0p5())
+            .h(rems(14. / 16.))
+            .text_ui()
+            .line_height(relative(1.))
             .text_color(cx.theme().colors().text)
-            .bg(cx.theme().colors().element_background)
             .child(self.key.clone())
     }
 }
@@ -69,3 +106,24 @@ impl Key {
         Self { key: key.into() }
     }
 }
+
+#[derive(IntoElement)]
+pub struct KeyIcon {
+    icon: Icon,
+}
+
+impl RenderOnce for KeyIcon {
+    type Rendered = Div;
+
+    fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
+        div()
+            .w(rems(14. / 16.))
+            .child(IconElement::new(self.icon).size(IconSize::Small))
+    }
+}
+
+impl KeyIcon {
+    pub fn new(icon: Icon) -> Self {
+        Self { icon }
+    }
+}

crates/ui2/src/components/label.rs 🔗

@@ -1,6 +1,6 @@
 use crate::prelude::*;
 use crate::styled_ext::StyledExt;
-use gpui::{relative, Div, Hsla, IntoElement, StyledText, TextRun, WindowContext};
+use gpui::{relative, Div, IntoElement, StyledText, TextRun, WindowContext};
 
 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
 pub enum LabelSize {
@@ -182,9 +182,3 @@ impl HighlightedLabel {
         self
     }
 }
-
-/// A run of text that receives the same style.
-struct Run {
-    pub text: String,
-    pub color: Hsla,
-}

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

@@ -1,25 +1,19 @@
+use std::rc::Rc;
+
 use gpui::{
-    div, px, AnyElement, ClickEvent, Div, IntoElement, Stateful, StatefulInteractiveElement,
+    div, px, AnyElement, ClickEvent, Div, ImageSource, IntoElement, MouseButton, MouseDownEvent,
+    Pixels, Stateful, StatefulInteractiveElement,
 };
 use smallvec::SmallVec;
-use std::rc::Rc;
 
 use crate::{
-    disclosure_control, h_stack, v_stack, Avatar, Icon, IconElement, IconSize, Label, Toggle,
+    disclosure_control, h_stack, v_stack, Avatar, Icon, IconButton, IconElement, IconSize, Label,
+    Toggle,
 };
 use crate::{prelude::*, GraphicSlot};
 
-#[derive(Clone, Copy, Default, Debug, PartialEq)]
-pub enum ListItemVariant {
-    /// The list item extends to the far left and right of the list.
-    FullWidth,
-    #[default]
-    Inset,
-}
-
 pub enum ListHeaderMeta {
-    // TODO: These should be IconButtons
-    Tools(Vec<Icon>),
+    Tools(Vec<IconButton>),
     // TODO: This should be a button
     Button(Label),
     Text(Label),
@@ -30,8 +24,39 @@ pub struct ListHeader {
     label: SharedString,
     left_icon: Option<Icon>,
     meta: Option<ListHeaderMeta>,
-    variant: ListItemVariant,
     toggle: Toggle,
+    inset: bool,
+}
+
+impl ListHeader {
+    pub fn new(label: impl Into<SharedString>) -> Self {
+        Self {
+            label: label.into(),
+            left_icon: None,
+            meta: None,
+            inset: false,
+            toggle: Toggle::NotToggleable,
+        }
+    }
+
+    pub fn toggle(mut self, toggle: Toggle) -> Self {
+        self.toggle = toggle;
+        self
+    }
+
+    pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
+        self.left_icon = left_icon;
+        self
+    }
+
+    pub fn right_button(self, button: IconButton) -> Self {
+        self.meta(Some(ListHeaderMeta::Tools(vec![button])))
+    }
+
+    pub fn meta(mut self, meta: Option<ListHeaderMeta>) -> Self {
+        self.meta = meta;
+        self
+    }
 }
 
 impl RenderOnce for ListHeader {
@@ -45,11 +70,7 @@ impl RenderOnce for ListHeader {
                 h_stack()
                     .gap_2()
                     .items_center()
-                    .children(icons.into_iter().map(|i| {
-                        IconElement::new(i)
-                            .color(Color::Muted)
-                            .size(IconSize::Small)
-                    })),
+                    .children(icons.into_iter().map(|i| i.color(Color::Muted))),
             ),
             Some(ListHeaderMeta::Button(label)) => div().child(label),
             Some(ListHeaderMeta::Text(label)) => div().child(label),
@@ -63,7 +84,7 @@ impl RenderOnce for ListHeader {
             .child(
                 div()
                     .h_5()
-                    .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
+                    .when(self.inset, |this| this.px_2())
                     .flex()
                     .flex_1()
                     .items_center()
@@ -92,98 +113,11 @@ impl RenderOnce for ListHeader {
     }
 }
 
-impl ListHeader {
-    pub fn new(label: impl Into<SharedString>) -> Self {
-        Self {
-            label: label.into(),
-            left_icon: None,
-            meta: None,
-            variant: ListItemVariant::default(),
-            toggle: Toggle::NotToggleable,
-        }
-    }
-
-    pub fn toggle(mut self, toggle: Toggle) -> Self {
-        self.toggle = toggle;
-        self
-    }
-
-    pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
-        self.left_icon = left_icon;
-        self
-    }
-
-    pub fn meta(mut self, meta: Option<ListHeaderMeta>) -> Self {
-        self.meta = meta;
-        self
-    }
-
-    // before_ship!("delete")
-    // fn render<V: 'static>(self,  cx: &mut WindowContext) -> impl Element<V> {
-    //     let disclosure_control = disclosure_control(self.toggle);
-
-    //     let meta = match self.meta {
-    //         Some(ListHeaderMeta::Tools(icons)) => div().child(
-    //             h_stack()
-    //                 .gap_2()
-    //                 .items_center()
-    //                 .children(icons.into_iter().map(|i| {
-    //                     IconElement::new(i)
-    //                         .color(TextColor::Muted)
-    //                         .size(IconSize::Small)
-    //                 })),
-    //         ),
-    //         Some(ListHeaderMeta::Button(label)) => div().child(label),
-    //         Some(ListHeaderMeta::Text(label)) => div().child(label),
-    //         None => div(),
-    //     };
-
-    //     h_stack()
-    //         .w_full()
-    //         .bg(cx.theme().colors().surface_background)
-    //         // TODO: Add focus state
-    //         // .when(self.state == InteractionState::Focused, |this| {
-    //         //     this.border()
-    //         //         .border_color(cx.theme().colors().border_focused)
-    //         // })
-    //         .relative()
-    //         .child(
-    //             div()
-    //                 .h_5()
-    //                 .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
-    //                 .flex()
-    //                 .flex_1()
-    //                 .items_center()
-    //                 .justify_between()
-    //                 .w_full()
-    //                 .gap_1()
-    //                 .child(
-    //                     h_stack()
-    //                         .gap_1()
-    //                         .child(
-    //                             div()
-    //                                 .flex()
-    //                                 .gap_1()
-    //                                 .items_center()
-    //                                 .children(self.left_icon.map(|i| {
-    //                                     IconElement::new(i)
-    //                                         .color(TextColor::Muted)
-    //                                         .size(IconSize::Small)
-    //                                 }))
-    //                                 .child(Label::new(self.label.clone()).color(TextColor::Muted)),
-    //                         )
-    //                         .child(disclosure_control),
-    //                 )
-    //                 .child(meta),
-    //         )
-    // }
-}
-
 #[derive(IntoElement, Clone)]
 pub struct ListSubHeader {
     label: SharedString,
     left_icon: Option<Icon>,
-    variant: ListItemVariant,
+    inset: bool,
 }
 
 impl ListSubHeader {
@@ -191,7 +125,7 @@ impl ListSubHeader {
         Self {
             label: label.into(),
             left_icon: None,
-            variant: ListItemVariant::default(),
+            inset: false,
         }
     }
 
@@ -204,11 +138,11 @@ impl ListSubHeader {
 impl RenderOnce for ListSubHeader {
     type Rendered = Div;
 
-    fn render(self, cx: &mut WindowContext) -> Self::Rendered {
+    fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
         h_stack().flex_1().w_full().relative().py_1().child(
             div()
                 .h_6()
-                .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
+                .when(self.inset, |this| this.px_2())
                 .flex()
                 .flex_1()
                 .w_full()
@@ -231,26 +165,19 @@ impl RenderOnce for ListSubHeader {
     }
 }
 
-#[derive(Default, PartialEq, Copy, Clone)]
-pub enum ListEntrySize {
-    #[default]
-    Small,
-    Medium,
-}
-
 #[derive(IntoElement)]
 pub struct ListItem {
     id: ElementId,
-    disabled: bool,
+    selected: bool,
     // TODO: Reintroduce this
     // disclosure_control_style: DisclosureControlVisibility,
-    indent_level: u32,
+    indent_level: usize,
+    indent_step_size: Pixels,
     left_slot: Option<GraphicSlot>,
-    overflow: OverflowStyle,
-    size: ListEntrySize,
     toggle: Toggle,
-    variant: ListItemVariant,
+    inset: bool,
     on_click: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
+    on_secondary_mouse_down: Option<Rc<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
     children: SmallVec<[AnyElement; 2]>,
 }
 
@@ -258,14 +185,14 @@ impl ListItem {
     pub fn new(id: impl Into<ElementId>) -> Self {
         Self {
             id: id.into(),
-            disabled: false,
+            selected: false,
             indent_level: 0,
+            indent_step_size: px(12.),
             left_slot: None,
-            overflow: OverflowStyle::Hidden,
-            size: ListEntrySize::default(),
             toggle: Toggle::NotToggleable,
-            variant: ListItemVariant::default(),
-            on_click: Default::default(),
+            inset: false,
+            on_click: None,
+            on_secondary_mouse_down: None,
             children: SmallVec::new(),
         }
     }
@@ -275,21 +202,39 @@ impl ListItem {
         self
     }
 
-    pub fn variant(mut self, variant: ListItemVariant) -> Self {
-        self.variant = variant;
+    pub fn on_secondary_mouse_down(
+        mut self,
+        handler: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
+    ) -> Self {
+        self.on_secondary_mouse_down = Some(Rc::new(handler));
         self
     }
 
-    pub fn indent_level(mut self, indent_level: u32) -> Self {
+    pub fn inset(mut self, inset: bool) -> Self {
+        self.inset = inset;
+        self
+    }
+
+    pub fn indent_level(mut self, indent_level: usize) -> Self {
         self.indent_level = indent_level;
         self
     }
 
+    pub fn indent_step_size(mut self, indent_step_size: Pixels) -> Self {
+        self.indent_step_size = indent_step_size;
+        self
+    }
+
     pub fn toggle(mut self, toggle: Toggle) -> Self {
         self.toggle = toggle;
         self
     }
 
+    pub fn selected(mut self, selected: bool) -> Self {
+        self.selected = selected;
+        self
+    }
+
     pub fn left_content(mut self, left_content: GraphicSlot) -> Self {
         self.left_slot = Some(left_content);
         self
@@ -300,15 +245,10 @@ impl ListItem {
         self
     }
 
-    pub fn left_avatar(mut self, left_avatar: impl Into<SharedString>) -> Self {
+    pub fn left_avatar(mut self, left_avatar: impl Into<ImageSource>) -> Self {
         self.left_slot = Some(GraphicSlot::Avatar(left_avatar.into()));
         self
     }
-
-    pub fn size(mut self, size: ListEntrySize) -> Self {
-        self.size = size;
-        self
-    }
 }
 
 impl RenderOnce for ListItem {
@@ -323,61 +263,64 @@ impl RenderOnce for ListItem {
                         .color(Color::Muted),
                 ),
             ),
-            Some(GraphicSlot::Avatar(src)) => Some(h_stack().child(Avatar::uri(src))),
+            Some(GraphicSlot::Avatar(src)) => Some(h_stack().child(Avatar::source(src))),
             Some(GraphicSlot::PublicActor(src)) => Some(h_stack().child(Avatar::uri(src))),
             None => None,
         };
 
-        let sized_item = match self.size {
-            ListEntrySize::Small => div().h_6(),
-            ListEntrySize::Medium => div().h_7(),
-        };
         div()
             .id(self.id)
             .relative()
-            .hover(|mut style| {
-                style.background = Some(cx.theme().colors().editor_background.into());
-                style
-            })
-            .on_click({
-                let on_click = self.on_click.clone();
-                move |event, cx| {
-                    if let Some(on_click) = &on_click {
-                        (on_click)(event, cx)
-                    }
-                }
-            })
             // TODO: Add focus state
             // .when(self.state == InteractionState::Focused, |this| {
             //     this.border()
             //         .border_color(cx.theme().colors().border_focused)
             // })
+            .when(self.inset, |this| this.rounded_md())
             .hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
             .active(|style| style.bg(cx.theme().colors().ghost_element_active))
+            .when(self.selected, |this| {
+                this.bg(cx.theme().colors().ghost_element_selected)
+            })
+            .when_some(self.on_click.clone(), |this, on_click| {
+                this.on_click(move |event, cx| {
+                    // HACK: GPUI currently fires `on_click` with any mouse button,
+                    // but we only care about the left button.
+                    if event.down.button == MouseButton::Left {
+                        (on_click)(event, cx)
+                    }
+                })
+            })
+            .when_some(self.on_secondary_mouse_down, |this, on_mouse_down| {
+                this.on_mouse_down(MouseButton::Right, move |event, cx| {
+                    (on_mouse_down)(event, cx)
+                })
+            })
             .child(
-                sized_item
-                    .when(self.variant == ListItemVariant::Inset, |this| this.px_2())
-                    // .ml(rems(0.75 * self.indent_level as f32))
-                    .children((0..self.indent_level).map(|_| {
-                        div()
-                            .w(px(4.))
-                            .h_full()
-                            .flex()
-                            .justify_center()
-                            .group_hover("", |style| style.bg(cx.theme().colors().border_focused))
-                            .child(
-                                h_stack()
-                                    .child(div().w_px().h_full())
-                                    .child(div().w_px().h_full().bg(cx.theme().colors().border)),
-                            )
-                    }))
+                div()
+                    .when(self.inset, |this| this.px_2())
+                    .ml(self.indent_level as f32 * self.indent_step_size)
                     .flex()
                     .gap_1()
                     .items_center()
                     .relative()
                     .child(disclosure_control(self.toggle))
                     .children(left_content)
-                    .children(self.children),
+                    .children(self.children)
+                    // HACK: We need to attach the `on_click` handler to the child element in order to have the click
+                    // event actually fire.
+                    // Once this is fixed in GPUI we can remove this and rely on the `on_click` handler set above on the
+                    // outer `div`.
+                    .id("on_click_hack")
+                    .when_some(self.on_click, |this, on_click| {
+                        this.on_click(move |event, cx| {
+                            // HACK: GPUI currently fires `on_click` with any mouse button,
+                            // but we only care about the left button.
+                            if event.down.button == MouseButton::Left {
+                                (on_click)(event, cx)
+                            }
+                        })
+                    }),
             )
     }
 }
@@ -418,7 +361,7 @@ pub struct List {
 impl RenderOnce for List {
     type Rendered = Div;
 
-    fn render(self, cx: &mut WindowContext) -> Self::Rendered {
+    fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
         let list_content = match (self.children.is_empty(), self.toggle) {
             (false, _) => div().children(self.children),
             (true, Toggle::Toggled(false)) => div(),

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

@@ -1,10 +1,11 @@
 use gpui::{
-    AnyElement, Div, Element, ElementId, IntoElement, ParentElement, RenderOnce, Styled,
+    div, AnyElement, Div, Element, ElementId, IntoElement, ParentElement, RenderOnce, Styled,
     WindowContext,
 };
 use smallvec::SmallVec;
 
-use crate::{v_stack, StyledExt};
+use crate::prelude::*;
+use crate::v_stack;
 
 /// A popover is used to display a menu or show some options.
 ///
@@ -43,22 +44,16 @@ impl RenderOnce for Popover {
     type Rendered = Div;
 
     fn render(self, cx: &mut WindowContext) -> Self::Rendered {
-        v_stack()
-            .relative()
-            .elevation_2(cx)
-            .p_1()
-            .children(self.children)
+        div()
+            .flex()
+            .gap_1()
+            .child(v_stack().elevation_2(cx).px_1().children(self.children))
             .when_some(self.aside, |this, aside| {
-                // TODO: This will statically position the aside to the top right of the popover.
-                // We should update this to use gpui2::overlay avoid collisions with the window edges.
                 this.child(
                     v_stack()
-                        .top_0()
-                        .left_full()
-                        .ml_1()
-                        .absolute()
                         .elevation_2(cx)
-                        .p_1()
+                        .bg(cx.theme().colors().surface_background)
+                        .px_1()
                         .child(aside),
                 )
             })

crates/ui2/src/components/slot.rs 🔗

@@ -1,4 +1,4 @@
-use gpui::SharedString;
+use gpui::{ImageSource, SharedString};
 
 use crate::Icon;
 
@@ -9,6 +9,6 @@ use crate::Icon;
 /// Can be filled with a []
 pub enum GraphicSlot {
     Icon(Icon),
-    Avatar(SharedString),
+    Avatar(ImageSource),
     PublicActor(SharedString),
 }

crates/ui2/src/components/stories.rs 🔗

@@ -3,6 +3,7 @@ mod button;
 mod checkbox;
 mod context_menu;
 mod icon;
+mod icon_button;
 mod input;
 mod keybinding;
 mod label;
@@ -13,6 +14,7 @@ pub use button::*;
 pub use checkbox::*;
 pub use context_menu::*;
 pub use icon::*;
+pub use icon_button::*;
 pub use input::*;
 pub use keybinding::*;
 pub use label::*;

crates/ui2/src/components/stories/avatar.rs 🔗

@@ -9,7 +9,7 @@ pub struct AvatarStory;
 impl Render for AvatarStory {
     type Element = Div;
 
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
         Story::container()
             .child(Story::title_for::<Avatar>())
             .child(Story::label("Default"))

crates/ui2/src/components/stories/button.rs 🔗

@@ -10,7 +10,7 @@ pub struct ButtonStory;
 impl Render for ButtonStory {
     type Element = Div;
 
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
         let states = InteractionState::iter();
 
         Story::container()
@@ -139,7 +139,7 @@ impl Render for ButtonStory {
             .child(
                 Button::new("Label")
                     .variant(ButtonVariant::Ghost)
-                    .on_click(|_, cx| println!("Button clicked.")),
+                    .on_click(|_, _cx| println!("Button clicked.")),
             )
     }
 }

crates/ui2/src/components/stories/context_menu.rs 🔗

@@ -10,11 +10,11 @@ fn build_menu(cx: &mut WindowContext, header: impl Into<SharedString>) -> View<C
     ContextMenu::build(cx, |menu, _| {
         menu.header(header)
             .separator()
-            .entry("Print current time", |v, cx| {
+            .entry("Print current time", |_event, cx| {
                 println!("dispatching PrintCurrentTime action");
                 cx.dispatch_action(PrintCurrentDate.boxed_clone())
             })
-            .entry("Print best foot", |v, cx| {
+            .entry("Print best foot", |_event, cx| {
                 cx.dispatch_action(PrintBestFood.boxed_clone())
             })
     })
@@ -25,7 +25,7 @@ pub struct ContextMenuStory;
 impl Render for ContextMenuStory {
     type Element = Div;
 
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
         Story::container()
             .on_action(|_: &PrintCurrentDate, _| {
                 println!("printing unix time!");

crates/ui2/src/components/stories/icon.rs 🔗

@@ -10,7 +10,7 @@ pub struct IconStory;
 impl Render for IconStory {
     type Element = Div;
 
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
         let icons = Icon::iter();
 
         Story::container()

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

@@ -0,0 +1,35 @@
+use gpui::{Div, Render};
+use story::Story;
+
+use crate::{prelude::*, Tooltip};
+use crate::{Icon, IconButton};
+
+pub struct IconButtonStory;
+
+impl Render for IconButtonStory {
+    type Element = Div;
+
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
+        Story::container()
+            .child(Story::title_for::<IconButton>())
+            .child(Story::label("Default"))
+            .child(div().w_8().child(IconButton::new("icon_a", Icon::Hash)))
+            .child(Story::label("With `on_click`"))
+            .child(
+                div()
+                    .w_8()
+                    .child(
+                        IconButton::new("with_on_click", Icon::Ai).on_click(|_event, _cx| {
+                            println!("Clicked!");
+                        }),
+                    ),
+            )
+            .child(Story::label("With `tooltip`"))
+            .child(
+                div().w_8().child(
+                    IconButton::new("with_tooltip", Icon::MessageBubbles)
+                        .tooltip(|cx| Tooltip::text("Open messages", cx)),
+                ),
+            )
+    }
+}

crates/ui2/src/components/stories/input.rs 🔗

@@ -9,7 +9,7 @@ pub struct InputStory;
 impl Render for InputStory {
     type Element = Div;
 
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
         Story::container()
             .child(Story::title_for::<Input>())
             .child(Story::label("Default"))

crates/ui2/src/components/stories/keybinding.rs 🔗

@@ -16,7 +16,7 @@ pub fn binding(key: &str) -> gpui::KeyBinding {
 impl Render for KeybindingStory {
     type Element = Div;
 
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
         let all_modifier_permutations = ["ctrl", "alt", "cmd", "shift"].into_iter().permutations(2);
 
         Story::container()

crates/ui2/src/components/stories/label.rs 🔗

@@ -9,7 +9,7 @@ pub struct LabelStory;
 impl Render for LabelStory {
     type Element = Div;
 
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
         Story::container()
             .child(Story::title_for::<Label>())
             .child(Story::label("Default"))

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

@@ -9,7 +9,7 @@ pub struct ListItemStory;
 impl Render for ListItemStory {
     type Element = Div;
 
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
         Story::container()
             .child(Story::title_for::<ListItem>())
             .child(Story::label("Default"))
@@ -22,5 +22,13 @@ impl Render for ListItemStory {
                         println!("Clicked!");
                     }),
             )
+            .child(Story::label("With `on_secondary_mouse_down`"))
+            .child(
+                ListItem::new("with_on_secondary_mouse_down").on_secondary_mouse_down(
+                    |_event, _cx| {
+                        println!("Right mouse down!");
+                    },
+                ),
+            )
     }
 }

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

@@ -1,6 +1,6 @@
 use gpui::{overlay, Action, AnyView, IntoElement, Overlay, Render, VisualContext};
-use settings2::Settings;
-use theme2::{ActiveTheme, ThemeSettings};
+use settings::Settings;
+use theme::ThemeSettings;
 
 use crate::prelude::*;
 use crate::{h_stack, v_stack, Color, KeyBinding, Label, LabelSize, StyledExt};
@@ -13,7 +13,7 @@ pub struct Tooltip {
 
 impl Tooltip {
     pub fn text(title: impl Into<SharedString>, cx: &mut WindowContext) -> AnyView {
-        cx.build_view(|cx| Self {
+        cx.build_view(|_cx| Self {
             title: title.into(),
             meta: None,
             key_binding: None,

crates/ui2/src/prelude.rs 🔗

@@ -5,7 +5,7 @@ pub use gpui::{
 
 pub use crate::StyledExt;
 pub use crate::{ButtonVariant, Color};
-pub use theme2::ActiveTheme;
+pub use theme::ActiveTheme;
 
 use strum::EnumIter;
 
@@ -16,12 +16,6 @@ pub enum IconSide {
     Right,
 }
 
-#[derive(Debug, PartialEq, Eq, Clone, Copy, EnumIter)]
-pub enum OverflowStyle {
-    Hidden,
-    Wrap,
-}
-
 #[derive(Default, PartialEq, Copy, Clone, EnumIter, strum::Display)]
 pub enum InteractionState {
     /// An element that is enabled and not hovered, active, focused, or disabled.

crates/ui2/src/styled_ext.rs 🔗

@@ -1,12 +1,12 @@
-use gpui::{Styled, WindowContext};
-use theme2::ActiveTheme;
+use gpui::{px, Styled, WindowContext};
 
+use crate::prelude::*;
 use crate::{ElevationIndex, UITextSize};
 
 fn elevated<E: Styled>(this: E, cx: &mut WindowContext, index: ElevationIndex) -> E {
     this.bg(cx.theme().colors().elevated_surface_background)
         .z_index(index.z_index())
-        .rounded_lg()
+        .rounded(px(8.))
         .border()
         .border_color(cx.theme().colors().border_variant)
         .shadow(index.shadow())

crates/ui2/src/styles/color.rs 🔗

@@ -1,5 +1,5 @@
 use gpui::{Hsla, WindowContext};
-use theme2::ActiveTheme;
+use theme::ActiveTheme;
 
 #[derive(Default, PartialEq, Copy, Clone)]
 pub enum Color {

crates/ui2/src/ui2.rs 🔗

@@ -11,8 +11,6 @@
 #![doc = include_str!("../docs/hello-world.md")]
 #![doc = include_str!("../docs/building-ui.md")]
 #![doc = include_str!("../docs/todo.md")]
-// TODO: Fix warnings instead of supressing.
-#![allow(dead_code, unused_variables)]
 
 mod components;
 pub mod prelude;

crates/ui2/src/utils/format_distance.rs 🔗

@@ -16,55 +16,54 @@ fn distance_in_seconds(date: NaiveDateTime, base_date: NaiveDateTime) -> i64 {
 fn distance_string(distance: i64, include_seconds: bool, add_suffix: bool) -> String {
     let suffix = if distance < 0 { " from now" } else { " ago" };
 
-    let d = distance.abs();
+    let distance = distance.abs();
 
-    let minutes = d / 60;
-    let hours = d / 3600;
-    let days = d / 86400;
-    let months = d / 2592000;
-    let years = d / 31536000;
+    let minutes = distance / 60;
+    let hours = distance / 3_600;
+    let days = distance / 86_400;
+    let months = distance / 2_592_000;
 
-    let string = if d < 5 && include_seconds {
+    let string = if distance < 5 && include_seconds {
         "less than 5 seconds".to_string()
-    } else if d < 10 && include_seconds {
+    } else if distance < 10 && include_seconds {
         "less than 10 seconds".to_string()
-    } else if d < 20 && include_seconds {
+    } else if distance < 20 && include_seconds {
         "less than 20 seconds".to_string()
-    } else if d < 40 && include_seconds {
+    } else if distance < 40 && include_seconds {
         "half a minute".to_string()
-    } else if d < 60 && include_seconds {
+    } else if distance < 60 && include_seconds {
         "less than a minute".to_string()
-    } else if d < 90 && include_seconds {
+    } else if distance < 90 && include_seconds {
         "1 minute".to_string()
-    } else if d < 30 {
+    } else if distance < 30 {
         "less than a minute".to_string()
-    } else if d < 90 {
+    } else if distance < 90 {
         "1 minute".to_string()
-    } else if d < 2700 {
+    } else if distance < 2_700 {
         format!("{} minutes", minutes)
-    } else if d < 5400 {
+    } else if distance < 5_400 {
         "about 1 hour".to_string()
-    } else if d < 86400 {
+    } else if distance < 86_400 {
         format!("about {} hours", hours)
-    } else if d < 172800 {
+    } else if distance < 172_800 {
         "1 day".to_string()
-    } else if d < 2592000 {
+    } else if distance < 2_592_000 {
         format!("{} days", days)
-    } else if d < 5184000 {
+    } else if distance < 5_184_000 {
         "about 1 month".to_string()
-    } else if d < 7776000 {
+    } else if distance < 7_776_000 {
         "about 2 months".to_string()
-    } else if d < 31540000 {
+    } else if distance < 31_540_000 {
         format!("{} months", months)
-    } else if d < 39425000 {
+    } else if distance < 39_425_000 {
         "about 1 year".to_string()
-    } else if d < 55195000 {
+    } else if distance < 55_195_000 {
         "over 1 year".to_string()
-    } else if d < 63080000 {
+    } else if distance < 63_080_000 {
         "almost 2 years".to_string()
     } else {
-        let years = d / 31536000;
-        let remaining_months = (d % 31536000) / 2592000;
+        let years = distance / 31_536_000;
+        let remaining_months = (distance % 31_536_000) / 2_592_000;
 
         if remaining_months < 3 {
             format!("about {} years", years)
@@ -76,7 +75,7 @@ fn distance_string(distance: i64, include_seconds: bool, add_suffix: bool) -> St
     };
 
     if add_suffix {
-        return format!("{}{}", string, suffix);
+        format!("{}{}", string, suffix)
     } else {
         string
     }

crates/welcome2/Cargo.toml 🔗

@@ -16,6 +16,7 @@ editor = { package = "editor2", path = "../editor2" }
 fs = { package = "fs2", path = "../fs2" }
 fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
 gpui = { package = "gpui2", path = "../gpui2" }
+ui = { package = "ui2", path = "../ui2" }
 db = { package = "db2", path = "../db2" }
 install_cli = { package = "install_cli2", path = "../install_cli2" }
 project = { package = "project2", path = "../project2" }

crates/welcome2/src/base_keymap_picker.rs 🔗

@@ -1,13 +1,14 @@
 use super::base_keymap_setting::BaseKeymap;
 use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
 use gpui::{
-    actions, AppContext, DismissEvent, EventEmitter, FocusableView, IntoElement, Render, Task,
+    actions, AppContext, DismissEvent, EventEmitter, FocusableView, ParentElement, Render, Task,
     View, ViewContext, VisualContext, WeakView,
 };
-use picker::{simple_picker_match, Picker, PickerDelegate};
+use picker::{Picker, PickerDelegate};
 use project::Fs;
 use settings::{update_settings_file, Settings};
 use std::sync::Arc;
+use ui::ListItem;
 use util::ResultExt;
 use workspace::{ui::HighlightedLabel, Workspace};
 
@@ -97,6 +98,8 @@ impl BaseKeymapSelectorDelegate {
 }
 
 impl PickerDelegate for BaseKeymapSelectorDelegate {
+    type ListItem = ui::ListItem;
+
     fn placeholder_text(&self) -> Arc<str> {
         "Select a base keymap...".into()
     }
@@ -188,13 +191,18 @@ impl PickerDelegate for BaseKeymapSelectorDelegate {
         &self,
         ix: usize,
         selected: bool,
-        cx: &mut gpui::ViewContext<Picker<Self>>,
-    ) -> gpui::AnyElement {
+        _cx: &mut gpui::ViewContext<Picker<Self>>,
+    ) -> Option<Self::ListItem> {
         let keymap_match = &self.matches[ix];
 
-        simple_picker_match(selected, cx, |_cx| {
-            HighlightedLabel::new(keymap_match.string.clone(), keymap_match.positions.clone())
-                .into_any_element()
-        })
+        Some(
+            ListItem::new(ix)
+                .selected(selected)
+                .inset(true)
+                .child(HighlightedLabel::new(
+                    keymap_match.string.clone(),
+                    keymap_match.positions.clone(),
+                )),
+        )
     }
 }

crates/workspace2/src/modal_layer.rs 🔗

@@ -95,10 +95,6 @@ impl Render for ModalLayer {
                     .track_focus(&active_modal.focus_handle)
                     .child(
                         h_stack()
-                            // needed to prevent mouse events leaking to the
-                            // UI below. // todo! for gpui3.
-                            .on_any_mouse_down(|_, cx| cx.stop_propagation())
-                            .on_any_mouse_up(|_, cx| cx.stop_propagation())
                             .on_mouse_down_out(cx.listener(|this, _, cx| {
                                 this.hide_modal(cx);
                             }))

crates/workspace2/src/workspace2.rs 🔗

@@ -2640,12 +2640,11 @@ impl Workspace {
                     .flex_col()
                     .justify_end()
                     .gap_2()
-                    .children(self.notifications.iter().map(|(_, _, notification)| {
-                        div()
-                            .on_any_mouse_down(|_, cx| cx.stop_propagation())
-                            .on_any_mouse_up(|_, cx| cx.stop_propagation())
-                            .child(notification.to_any())
-                    })),
+                    .children(
+                        self.notifications
+                            .iter()
+                            .map(|(_, _, notification)| notification.to_any()),
+                    ),
             )
         }
     }

crates/zed/Cargo.toml 🔗

@@ -140,6 +140,7 @@ tree-sitter-lua.workspace = true
 tree-sitter-nix.workspace = true
 tree-sitter-nu.workspace = true
 tree-sitter-vue.workspace = true
+tree-sitter-uiua.workspace = true
 
 url = "2.2"
 urlencoding = "2.1.2"

crates/zed/src/languages.rs 🔗

@@ -17,6 +17,7 @@ mod json;
 #[cfg(feature = "plugin_runtime")]
 mod language_plugin;
 mod lua;
+mod nu;
 mod php;
 mod python;
 mod ruby;
@@ -24,6 +25,7 @@ mod rust;
 mod svelte;
 mod tailwind;
 mod typescript;
+mod uiua;
 mod vue;
 mod yaml;
 
@@ -210,12 +212,21 @@ pub fn init(
     language("elm", tree_sitter_elm::language(), vec![]);
     language("glsl", tree_sitter_glsl::language(), vec![]);
     language("nix", tree_sitter_nix::language(), vec![]);
-    language("nu", tree_sitter_nu::language(), vec![]);
+    language(
+        "nu",
+        tree_sitter_nu::language(),
+        vec![Arc::new(nu::NuLanguageServer {})],
+    );
     language(
         "vue",
         tree_sitter_vue::language(),
         vec![Arc::new(vue::VueLspAdapter::new(node_runtime))],
     );
+    language(
+        "uiua",
+        tree_sitter_uiua::language(),
+        vec![Arc::new(uiua::UiuaLanguageServer {})],
+    );
 }
 
 #[cfg(any(test, feature = "test-support"))]

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

@@ -0,0 +1,81 @@
+use anyhow::{anyhow, Result};
+use async_trait::async_trait;
+use language::{CodeLabel, Language, LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp::LanguageServerBinary;
+use std::{any::Any, path::PathBuf, sync::Arc};
+
+pub struct NuLanguageServer;
+
+#[async_trait]
+impl LspAdapter for NuLanguageServer {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("nu".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "nu"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Any + Send>> {
+        Ok(Box::new(()))
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        _version: Box<dyn 'static + Send + Any>,
+        _container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        Err(anyhow!(
+            "nu v0.87.0 or greater must be installed and available in your $PATH"
+        ))
+    }
+
+    async fn cached_server_binary(
+        &self,
+        _: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        Some(LanguageServerBinary {
+            path: "nu".into(),
+            arguments: vec!["--lsp".into()],
+        })
+    }
+
+    fn can_be_reinstalled(&self) -> bool {
+        false
+    }
+
+    async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
+        None
+    }
+
+    async fn label_for_completion(
+        &self,
+        completion: &lsp::CompletionItem,
+        language: &Arc<Language>,
+    ) -> Option<CodeLabel> {
+        return Some(CodeLabel {
+            runs: language
+                .highlight_text(&completion.label.clone().into(), 0..completion.label.len()),
+            text: completion.label.clone(),
+            filter_range: 0..completion.label.len(),
+        });
+    }
+
+    async fn label_for_symbol(
+        &self,
+        name: &str,
+        _: lsp::SymbolKind,
+        language: &Arc<Language>,
+    ) -> Option<CodeLabel> {
+        Some(CodeLabel {
+            runs: language.highlight_text(&name.into(), 0..name.len()),
+            text: name.to_string(),
+            filter_range: 0..name.len(),
+        })
+    }
+}

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

@@ -0,0 +1,55 @@
+use anyhow::{anyhow, Result};
+use async_trait::async_trait;
+use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp::LanguageServerBinary;
+use std::{any::Any, path::PathBuf};
+
+pub struct UiuaLanguageServer;
+
+#[async_trait]
+impl LspAdapter for UiuaLanguageServer {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("uiua".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "uiua"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Any + Send>> {
+        Ok(Box::new(()))
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        _version: Box<dyn 'static + Send + Any>,
+        _container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        Err(anyhow!(
+            "uiua must be installed and available in your $PATH"
+        ))
+    }
+
+    async fn cached_server_binary(
+        &self,
+        _: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        Some(LanguageServerBinary {
+            path: "uiua".into(),
+            arguments: vec!["lsp".into()],
+        })
+    }
+
+    fn can_be_reinstalled(&self) -> bool {
+        false
+    }
+
+    async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
+        None
+    }
+}

crates/zed/src/languages/uiua/config.toml 🔗

@@ -0,0 +1,10 @@
+name = "Uiua"
+path_suffixes = ["ua"]
+line_comment = "# "
+autoclose_before = ")]}\""
+brackets = [
+    { start = "{", end = "}", close = true, newline = false },
+    { start = "[", end = "]", close = true, newline = false },
+    { start = "(", end = ")", close = true, newline = false },
+    { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
+]

crates/zed/src/languages/uiua/highlights.scm 🔗

@@ -0,0 +1,50 @@
+[
+  (openParen)
+  (closeParen)
+  (openCurly)
+  (closeCurly)
+  (openBracket)
+  (closeBracket)
+] @punctuation.bracket
+
+[
+  (branchSeparator)
+  (underscore)
+] @constructor
+; ] @punctuation.delimiter
+
+[ (character) ] @constant.character
+[ (comment) ] @comment
+[ (constant) ] @constant.numeric
+[ (identifier) ] @variable
+[ (leftArrow) ] @keyword
+[ (function) ] @function
+[ (modifier1) ] @operator
+[ (modifier2) ] @operator
+[ (number) ] @constant.numeric
+[ (placeHolder) ] @special
+[ (otherConstant) ] @string.special
+[ (signature) ] @type
+[ (system) ] @function.builtin
+[ (tripleMinus) ] @module
+
+; planet
+[
+  "id"
+  "identity"
+  "∘"
+  "dip"
+  "⊙"
+  "gap"
+  "⋅"
+] @tag
+
+[
+  (string)
+  (multiLineString)
+] @string
+
+; [
+;   (deprecated)
+;   (identifierDeprecated)
+; ] @warning

crates/zed2/Cargo.toml 🔗

@@ -21,7 +21,7 @@ audio = { package = "audio2", path = "../audio2" }
 auto_update = { package = "auto_update2", path = "../auto_update2" }
 # breadcrumbs = { path = "../breadcrumbs" }
 call = { package = "call2", path = "../call2" }
-# channel = { path = "../channel" }
+channel = { package = "channel2", path = "../channel2" }
 cli = { path = "../cli" }
 collab_ui = { package = "collab_ui2", path = "../collab_ui2" }
 collections = { path = "../collections" }
@@ -136,6 +136,7 @@ tree-sitter-lua.workspace = true
 tree-sitter-nix.workspace = true
 tree-sitter-nu.workspace = true
 tree-sitter-vue.workspace = true
+tree-sitter-uiua.workspace = true
 
 url = "2.2"
 urlencoding = "2.1.2"

crates/zed2/src/languages.rs 🔗

@@ -18,6 +18,7 @@ mod json;
 #[cfg(feature = "plugin_runtime")]
 mod language_plugin;
 mod lua;
+mod nu;
 mod php;
 mod python;
 mod ruby;
@@ -25,6 +26,7 @@ mod rust;
 mod svelte;
 mod tailwind;
 mod typescript;
+mod uiua;
 mod vue;
 mod yaml;
 
@@ -211,12 +213,21 @@ pub fn init(
     language("elm", tree_sitter_elm::language(), vec![]);
     language("glsl", tree_sitter_glsl::language(), vec![]);
     language("nix", tree_sitter_nix::language(), vec![]);
-    language("nu", tree_sitter_nu::language(), vec![]);
+    language(
+        "nu",
+        tree_sitter_nu::language(),
+        vec![Arc::new(nu::NuLanguageServer {})],
+    );
     language(
         "vue",
         tree_sitter_vue::language(),
         vec![Arc::new(vue::VueLspAdapter::new(node_runtime))],
     );
+    language(
+        "uiua",
+        tree_sitter_uiua::language(),
+        vec![Arc::new(uiua::UiuaLanguageServer {})],
+    );
 }
 
 #[cfg(any(test, feature = "test-support"))]

crates/zed2/src/languages/nu.rs 🔗

@@ -0,0 +1,55 @@
+use anyhow::{anyhow, Result};
+use async_trait::async_trait;
+use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp::LanguageServerBinary;
+use std::{any::Any, path::PathBuf};
+
+pub struct NuLanguageServer;
+
+#[async_trait]
+impl LspAdapter for NuLanguageServer {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("nu".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "nu"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Any + Send>> {
+        Ok(Box::new(()))
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        _version: Box<dyn 'static + Send + Any>,
+        _container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        Err(anyhow!(
+            "nu v0.87.0 or greater must be installed and available in your $PATH"
+        ))
+    }
+
+    async fn cached_server_binary(
+        &self,
+        _: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        Some(LanguageServerBinary {
+            path: "nu".into(),
+            arguments: vec!["--lsp".into()],
+        })
+    }
+
+    fn can_be_reinstalled(&self) -> bool {
+        false
+    }
+
+    async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
+        None
+    }
+}

crates/zed2/src/languages/uiua.rs 🔗

@@ -0,0 +1,55 @@
+use anyhow::{anyhow, Result};
+use async_trait::async_trait;
+use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp::LanguageServerBinary;
+use std::{any::Any, path::PathBuf};
+
+pub struct UiuaLanguageServer;
+
+#[async_trait]
+impl LspAdapter for UiuaLanguageServer {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("uiua".into())
+    }
+
+    fn short_name(&self) -> &'static str {
+        "uiua"
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<Box<dyn 'static + Any + Send>> {
+        Ok(Box::new(()))
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        _version: Box<dyn 'static + Send + Any>,
+        _container_dir: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        Err(anyhow!(
+            "uiua must be installed and available in your $PATH"
+        ))
+    }
+
+    async fn cached_server_binary(
+        &self,
+        _: PathBuf,
+        _: &dyn LspAdapterDelegate,
+    ) -> Option<LanguageServerBinary> {
+        Some(LanguageServerBinary {
+            path: "uiua".into(),
+            arguments: vec!["lsp".into()],
+        })
+    }
+
+    fn can_be_reinstalled(&self) -> bool {
+        false
+    }
+
+    async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
+        None
+    }
+}

crates/zed2/src/languages/uiua/config.toml 🔗

@@ -0,0 +1,10 @@
+name = "Uiua"
+path_suffixes = ["ua"]
+line_comment = "# "
+autoclose_before = ")]}\""
+brackets = [
+    { start = "{", end = "}", close = true, newline = false},
+    { start = "[", end = "]", close = true, newline = false },
+    { start = "(", end = ")", close = true, newline = false },
+    { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
+]

crates/zed2/src/languages/uiua/highlights.scm 🔗

@@ -0,0 +1,50 @@
+[
+  (openParen)
+  (closeParen)
+  (openCurly)
+  (closeCurly)
+  (openBracket)
+  (closeBracket)
+] @punctuation.bracket
+
+[
+  (branchSeparator)
+  (underscore)
+] @constructor
+; ] @punctuation.delimiter
+
+[ (character) ] @constant.character
+[ (comment) ] @comment
+[ (constant) ] @constant.numeric
+[ (identifier) ] @variable
+[ (leftArrow) ] @keyword
+[ (function) ] @function
+[ (modifier1) ] @operator
+[ (modifier2) ] @operator
+[ (number) ] @constant.numeric
+[ (placeHolder) ] @special
+[ (otherConstant) ] @string.special
+[ (signature) ] @type
+[ (system) ] @function.builtin
+[ (tripleMinus) ] @module
+
+; planet
+[
+  "id"
+  "identity"
+  "∘"
+  "dip"
+  "⊙"
+  "gap"
+  "⋅"
+] @tag
+
+[
+  (string)
+  (multiLineString)
+] @string
+
+; [
+;   (deprecated)
+;   (identifierDeprecated)
+; ] @warning

crates/zed2/src/main.rs 🔗

@@ -188,7 +188,7 @@ fn main() {
         let app_state = Arc::new(AppState {
             languages,
             client: client.clone(),
-            user_store,
+            user_store: user_store.clone(),
             fs,
             build_window_options,
             call_factory: call::Call::new,
@@ -208,7 +208,7 @@ fn main() {
         // outline::init(cx);
         // project_symbols::init(cx);
         project_panel::init(Assets, cx);
-        // channel::init(&client, user_store.clone(), cx);
+        channel::init(&client, user_store.clone(), cx);
         // diagnostics::init(cx);
         search::init(cx);
         // semantic_index::init(fs.clone(), http.clone(), languages.clone(), cx);