Remove 2 suffix for welcome, vcs_menu, quick_action_bar, collab_ui

Max Brunsfeld and Mikayla created

Co-authored-by: Mikayla <mikayla@zed.dev>

Change summary

Cargo.lock                                                         |  100 
Cargo.toml                                                         |    5 
crates/collab2/Cargo.toml                                          |    4 
crates/collab_ui/Cargo.toml                                        |   69 
crates/collab_ui/src/channel_view.rs                               |  210 
crates/collab_ui/src/chat_panel.rs                                 |  724 
crates/collab_ui/src/chat_panel/message_editor.rs                  |  109 
crates/collab_ui/src/collab_panel.rs                               |  892 
crates/collab_ui/src/collab_panel/channel_modal.rs                 |  638 
crates/collab_ui/src/collab_panel/contact_finder.rs                |  228 
crates/collab_ui/src/collab_titlebar_item.rs                       | 1433 
crates/collab_ui/src/collab_ui.rs                                  |  124 
crates/collab_ui/src/face_pile.rs                                  |  123 
crates/collab_ui/src/notification_panel.rs                         |  653 
crates/collab_ui/src/notifications/incoming_call_notification.rs   |  252 
crates/collab_ui/src/notifications/project_shared_notification.rs  |  203 
crates/collab_ui/src/panel_settings.rs                             |   21 
crates/collab_ui2/Cargo.toml                                       |   81 
crates/collab_ui2/src/channel_view.rs                              |  448 
crates/collab_ui2/src/chat_panel.rs                                |  704 
crates/collab_ui2/src/chat_panel/message_editor.rs                 |  296 
crates/collab_ui2/src/collab_panel.rs                              | 2539 
crates/collab_ui2/src/collab_panel/channel_modal.rs                |  575 
crates/collab_ui2/src/collab_panel/contact_finder.rs               |  163 
crates/collab_ui2/src/collab_titlebar_item.rs                      |  586 
crates/collab_ui2/src/collab_ui.rs                                 |  167 
crates/collab_ui2/src/face_pile.rs                                 |   30 
crates/collab_ui2/src/notification_panel.rs                        |  755 
crates/collab_ui2/src/notifications.rs                             |   11 
crates/collab_ui2/src/notifications/incoming_call_notification.rs  |  163 
crates/collab_ui2/src/notifications/project_shared_notification.rs |  180 
crates/collab_ui2/src/panel_settings.rs                            |   70 
crates/quick_action_bar/Cargo.toml                                 |   19 
crates/quick_action_bar/src/quick_action_bar.rs                    |  238 
crates/quick_action_bar2/Cargo.toml                                |   22 
crates/quick_action_bar2/src/quick_action_bar.rs                   |  191 
crates/vcs_menu/Cargo.toml                                         |   12 
crates/vcs_menu/src/lib.rs                                         |  293 
crates/vcs_menu2/Cargo.toml                                        |   17 
crates/vcs_menu2/src/lib.rs                                        |  358 
crates/welcome/Cargo.toml                                          |   31 
crates/welcome/src/base_keymap_picker.rs                           |  124 
crates/welcome/src/base_keymap_setting.rs                          |    6 
crates/welcome/src/welcome.rs                                      |  428 
crates/welcome2/Cargo.toml                                         |   37 
crates/welcome2/src/base_keymap_picker.rs                          |  208 
crates/welcome2/src/base_keymap_setting.rs                         |   65 
crates/welcome2/src/welcome.rs                                     |  283 
crates/zed/Cargo.toml                                              |    6 
49 files changed, 2,450 insertions(+), 12,444 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1762,7 +1762,7 @@ dependencies = [
  "anyhow",
  "async-trait",
  "async-tungstenite",
- "audio2",
+ "audio",
  "axum",
  "axum-extra",
  "base64 0.13.1",
@@ -1771,7 +1771,7 @@ dependencies = [
  "clap 3.2.25",
  "client2",
  "clock",
- "collab_ui2",
+ "collab_ui",
  "collections",
  "ctor",
  "dashmap",
@@ -1831,53 +1831,6 @@ dependencies = [
 [[package]]
 name = "collab_ui"
 version = "0.1.0"
-dependencies = [
- "anyhow",
- "auto_update",
- "call",
- "channel",
- "client",
- "clock",
- "collections",
- "context_menu",
- "db",
- "drag_and_drop",
- "editor",
- "feature_flags",
- "feedback",
- "futures 0.3.28",
- "fuzzy",
- "gpui",
- "language",
- "lazy_static",
- "log",
- "menu",
- "notifications",
- "picker",
- "postage",
- "pretty_assertions",
- "project",
- "recent_projects",
- "rich_text",
- "rpc",
- "schemars",
- "serde",
- "serde_derive",
- "settings",
- "smallvec",
- "theme",
- "theme_selector",
- "time",
- "tree-sitter-markdown",
- "util",
- "vcs_menu",
- "workspace",
- "zed-actions",
-]
-
-[[package]]
-name = "collab_ui2"
-version = "0.1.0"
 dependencies = [
  "anyhow",
  "auto_update2",
@@ -1916,7 +1869,7 @@ dependencies = [
  "tree-sitter-markdown",
  "ui2",
  "util",
- "vcs_menu2",
+ "vcs_menu",
  "workspace2",
  "zed_actions2",
 ]
@@ -7010,7 +6963,7 @@ dependencies = [
 ]
 
 [[package]]
-name = "quick_action_bar2"
+name = "quick_action_bar"
 version = "0.1.0"
 dependencies = [
  "assistant2",
@@ -10604,20 +10557,6 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
 [[package]]
 name = "vcs_menu"
 version = "0.1.0"
-dependencies = [
- "anyhow",
- "fs",
- "fuzzy",
- "gpui",
- "picker",
- "theme",
- "util",
- "workspace",
-]
-
-[[package]]
-name = "vcs_menu2"
-version = "0.1.0"
 dependencies = [
  "anyhow",
  "fs2",
@@ -11093,31 +11032,6 @@ checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb"
 [[package]]
 name = "welcome"
 version = "0.1.0"
-dependencies = [
- "anyhow",
- "client",
- "db",
- "editor",
- "fs",
- "fuzzy",
- "gpui",
- "install_cli",
- "log",
- "picker",
- "project",
- "schemars",
- "serde",
- "settings",
- "theme",
- "theme_selector",
- "util",
- "vim",
- "workspace",
-]
-
-[[package]]
-name = "welcome2"
-version = "0.1.0"
 dependencies = [
  "anyhow",
  "client2",
@@ -11557,7 +11471,7 @@ dependencies = [
  "chrono",
  "cli",
  "client2",
- "collab_ui2",
+ "collab_ui",
  "collections",
  "command_palette2",
  "copilot2",
@@ -11598,7 +11512,7 @@ dependencies = [
  "project2",
  "project_panel2",
  "project_symbols2",
- "quick_action_bar2",
+ "quick_action_bar",
  "rand 0.8.5",
  "recent_projects2",
  "regex",
@@ -11661,7 +11575,7 @@ dependencies = [
  "util",
  "uuid 1.4.1",
  "vim2",
- "welcome2",
+ "welcome",
  "workspace2",
  "zed_actions2",
 ]

Cargo.toml 🔗

@@ -22,7 +22,6 @@ members = [
     "crates/collab",
     "crates/collab2",
     "crates/collab_ui",
-    "crates/collab_ui2",
     "crates/collections",
     "crates/command_palette",
     "crates/command_palette2",
@@ -91,7 +90,7 @@ members = [
     "crates/project_panel2",
     "crates/project_symbols",
     "crates/project_symbols2",
-    "crates/quick_action_bar2",
+    "crates/quick_action_bar",
     "crates/recent_projects",
     "crates/recent_projects2",
     "crates/rope",
@@ -123,10 +122,8 @@ members = [
     "crates/story",
     "crates/vim",
     "crates/vcs_menu",
-    "crates/vcs_menu2",
     "crates/workspace2",
     "crates/welcome",
-    "crates/welcome2",
     "crates/xtask",
     "crates/zed",
     "crates/zed-actions",

crates/collab2/Cargo.toml 🔗

@@ -60,7 +60,7 @@ tracing-subscriber = { version = "0.3.11", features = ["env-filter", "json"] }
 uuid.workspace = true
 
 [dev-dependencies]
-audio = { package = "audio2", path = "../audio2" }
+audio = { path = "../audio" }
 collections = { path = "../collections", features = ["test-support"] }
 gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
 call = { package = "call2", path = "../call2", features = ["test-support"] }
@@ -81,7 +81,7 @@ settings = { package = "settings2", path = "../settings2", features = ["test-sup
 theme = { package = "theme2", path = "../theme2" }
 workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
 
-collab_ui = { path = "../collab_ui2", package = "collab_ui2", features = ["test-support"] }
+collab_ui = { path = "../collab_ui", features = ["test-support"] }
 
 async-trait.workspace = true
 pretty_assertions.workspace = true

crates/collab_ui/Cargo.toml 🔗

@@ -22,35 +22,36 @@ test-support = [
 ]
 
 [dependencies]
-auto_update = { path = "../auto_update" }
-db = { path = "../db" }
-call = { path = "../call" }
-client = { path = "../client" }
-channel = { path = "../channel" }
+auto_update = { package = "auto_update2", path = "../auto_update2" }
+db = { package = "db2", path = "../db2" }
+call = { package = "call2", path = "../call2" }
+client = { package = "client2", path = "../client2" }
+channel = { package = "channel2", path = "../channel2" }
 clock = { path = "../clock" }
 collections = { path = "../collections" }
-context_menu = { path = "../context_menu" }
-drag_and_drop = { path = "../drag_and_drop" }
-editor = { path = "../editor" }
-feedback = { path = "../feedback" }
-fuzzy = { path = "../fuzzy" }
-gpui = { path = "../gpui" }
-language = { path = "../language" }
-menu = { path = "../menu" }
-notifications = { path = "../notifications" }
-rich_text = { path = "../rich_text" }
-picker = { path = "../picker" }
-project = { path = "../project" }
-recent_projects = { path = "../recent_projects" }
-rpc = { path = "../rpc" }
-settings = { path = "../settings" }
-feature_flags = {path = "../feature_flags"}
-theme = { path = "../theme" }
-theme_selector = { path = "../theme_selector" }
+# context_menu = { path = "../context_menu" }
+# drag_and_drop = { path = "../drag_and_drop" }
+editor = { package="editor2", path = "../editor2" }
+feedback = { package = "feedback2", path = "../feedback2" }
+fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
+gpui = { package = "gpui2", path = "../gpui2" }
+language = { package = "language2", path = "../language2" }
+menu = { package = "menu2",  path = "../menu2" }
+notifications = { package = "notifications2",  path = "../notifications2" }
+rich_text = { package = "rich_text2", path = "../rich_text2" }
+picker = { package = "picker2", path = "../picker2" }
+project = { package = "project2", path = "../project2" }
+recent_projects = { package = "recent_projects2", path = "../recent_projects2" }
+rpc = { package ="rpc2",  path = "../rpc2" }
+settings = { package = "settings2", path = "../settings2" }
+feature_flags = { package = "feature_flags2", path = "../feature_flags2"}
+theme = { package = "theme2", path = "../theme2" }
+theme_selector = { package = "theme_selector2", path = "../theme_selector2" }
 vcs_menu = { path = "../vcs_menu" }
+ui = { package = "ui2", path = "../ui2" }
 util = { path = "../util" }
-workspace = { path = "../workspace" }
-zed-actions = {path = "../zed-actions"}
+workspace = { package = "workspace2", path = "../workspace2" }
+zed-actions = { package="zed_actions2", path = "../zed_actions2"}
 
 anyhow.workspace = true
 futures.workspace = true
@@ -64,17 +65,17 @@ time.workspace = true
 smallvec.workspace = true
 
 [dev-dependencies]
-call = { path = "../call", features = ["test-support"] }
-client = { path = "../client", features = ["test-support"] }
+call = { package = "call2", path = "../call2", features = ["test-support"] }
+client = { package = "client2", path = "../client2", features = ["test-support"] }
 collections = { path = "../collections", features = ["test-support"] }
-editor = { path = "../editor", features = ["test-support"] }
-gpui = { path = "../gpui", features = ["test-support"] }
-notifications = { path = "../notifications", features = ["test-support"] }
-project = { path = "../project", features = ["test-support"] }
-rpc = { path = "../rpc", features = ["test-support"] }
-settings = { path = "../settings", features = ["test-support"] }
+editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
+gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
+notifications = { package = "notifications2", path = "../notifications2", features = ["test-support"] }
+project = { package = "project2", path = "../project2", features = ["test-support"] }
+rpc = { package = "rpc2", path = "../rpc2", features = ["test-support"] }
+settings = { package = "settings2", path = "../settings2", features = ["test-support"] }
 util = { path = "../util", features = ["test-support"] }
-workspace = { path = "../workspace", features = ["test-support"] }
+workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
 
 pretty_assertions.workspace = true
 tree-sitter-markdown.workspace = true

crates/collab_ui/src/channel_view.rs 🔗

@@ -1,4 +1,4 @@
-use anyhow::{anyhow, Result};
+use anyhow::Result;
 use call::report_call_event_for_channel;
 use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId, ChannelStore};
 use client::{
@@ -6,20 +6,18 @@ use client::{
     Collaborator, ParticipantIndex,
 };
 use collections::HashMap;
-use editor::{CollaborationHub, Editor};
+use editor::{CollaborationHub, Editor, EditorEvent};
 use gpui::{
-    actions,
-    elements::{ChildView, Label},
-    geometry::vector::Vector2F,
-    AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Subscription, Task, View,
-    ViewContext, ViewHandle,
+    actions, AnyElement, AnyView, AppContext, Entity as _, EventEmitter, FocusableView,
+    IntoElement as _, Model, Pixels, Point, Render, Subscription, Task, View, ViewContext,
+    VisualContext as _, WindowContext,
 };
 use project::Project;
-use smallvec::SmallVec;
 use std::{
     any::{Any, TypeId},
     sync::Arc,
 };
+use ui::{prelude::*, Label};
 use util::ResultExt;
 use workspace::{
     item::{FollowableItem, Item, ItemEvent, ItemHandle},
@@ -28,17 +26,17 @@ use workspace::{
     ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId,
 };
 
-actions!(channel_view, [Deploy]);
+actions!(collab, [Deploy]);
 
 pub fn init(cx: &mut AppContext) {
     register_followable_item::<ChannelView>(cx)
 }
 
 pub struct ChannelView {
-    pub editor: ViewHandle<Editor>,
-    project: ModelHandle<Project>,
-    channel_store: ModelHandle<ChannelStore>,
-    channel_buffer: ModelHandle<ChannelBuffer>,
+    pub editor: View<Editor>,
+    project: Model<Project>,
+    channel_store: Model<ChannelStore>,
+    channel_buffer: Model<ChannelBuffer>,
     remote_id: Option<ViewId>,
     _editor_event_subscription: Subscription,
 }
@@ -46,9 +44,9 @@ pub struct ChannelView {
 impl ChannelView {
     pub fn open(
         channel_id: ChannelId,
-        workspace: ViewHandle<Workspace>,
-        cx: &mut AppContext,
-    ) -> Task<Result<ViewHandle<Self>>> {
+        workspace: View<Workspace>,
+        cx: &mut WindowContext,
+    ) -> Task<Result<View<Self>>> {
         let pane = workspace.read(cx).active_pane().clone();
         let channel_view = Self::open_in_pane(channel_id, pane.clone(), workspace.clone(), cx);
         cx.spawn(|mut cx| async move {
@@ -61,17 +59,17 @@ impl ChannelView {
                     cx,
                 );
                 pane.add_item(Box::new(channel_view.clone()), true, true, None, cx);
-            });
+            })?;
             anyhow::Ok(channel_view)
         })
     }
 
     pub fn open_in_pane(
         channel_id: ChannelId,
-        pane: ViewHandle<Pane>,
-        workspace: ViewHandle<Workspace>,
-        cx: &mut AppContext,
-    ) -> Task<Result<ViewHandle<Self>>> {
+        pane: View<Pane>,
+        workspace: View<Workspace>,
+        cx: &mut WindowContext,
+    ) -> Task<Result<View<Self>>> {
         let workspace = workspace.read(cx);
         let project = workspace.project().to_owned();
         let channel_store = ChannelStore::global(cx);
@@ -91,7 +89,7 @@ impl ChannelView {
                         buffer.set_language(Some(markdown), cx);
                     }
                 })
-            });
+            })?;
 
             pane.update(&mut cx, |pane, cx| {
                 let buffer_id = channel_buffer.read(cx).remote_id(cx);
@@ -107,7 +105,7 @@ impl ChannelView {
                     }
                 }
 
-                let view = cx.add_view(|cx| {
+                let view = cx.new_view(|cx| {
                     let mut this = Self::new(project, channel_store, channel_buffer, cx);
                     this.acknowledge_buffer_version(cx);
                     this
@@ -117,7 +115,7 @@ impl ChannelView {
                 // replace that.
                 if let Some(existing_item) = existing_view {
                     if let Some(ix) = pane.index_for_item(&existing_item) {
-                        pane.close_item_by_id(existing_item.id(), SaveIntent::Skip, cx)
+                        pane.close_item_by_id(existing_item.entity_id(), SaveIntent::Skip, cx)
                             .detach();
                         pane.add_item(Box::new(view.clone()), true, true, Some(ix), cx);
                     }
@@ -125,18 +123,17 @@ impl ChannelView {
 
                 view
             })
-            .ok_or_else(|| anyhow!("pane was dropped"))
         })
     }
 
     pub fn new(
-        project: ModelHandle<Project>,
-        channel_store: ModelHandle<ChannelStore>,
-        channel_buffer: ModelHandle<ChannelBuffer>,
+        project: Model<Project>,
+        channel_store: Model<ChannelStore>,
+        channel_buffer: Model<ChannelBuffer>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
         let buffer = channel_buffer.read(cx).buffer();
-        let editor = cx.add_view(|cx| {
+        let editor = cx.new_view(|cx| {
             let mut editor = Editor::for_buffer(buffer, None, cx);
             editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub(
                 channel_buffer.clone(),
@@ -149,7 +146,8 @@ impl ChannelView {
             );
             editor
         });
-        let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone()));
+        let _editor_event_subscription =
+            cx.subscribe(&editor, |_, _, e: &EditorEvent, cx| cx.emit(e.clone()));
 
         cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event)
             .detach();
@@ -170,7 +168,7 @@ impl ChannelView {
 
     fn handle_channel_buffer_event(
         &mut self,
-        _: ModelHandle<ChannelBuffer>,
+        _: Model<ChannelBuffer>,
         event: &ChannelBufferEvent,
         cx: &mut ViewContext<Self>,
     ) {
@@ -182,12 +180,12 @@ impl ChannelView {
             ChannelBufferEvent::ChannelChanged => {
                 self.editor.update(cx, |editor, cx| {
                     editor.set_read_only(!self.channel(cx).is_some_and(|c| c.can_edit_notes()));
-                    cx.emit(editor::Event::TitleChanged);
+                    cx.emit(editor::EditorEvent::TitleChanged);
                     cx.notify()
                 });
             }
             ChannelBufferEvent::BufferEdited => {
-                if cx.is_self_focused() || self.editor.is_focused(cx) {
+                if self.editor.read(cx).is_focused(cx) {
                     self.acknowledge_buffer_version(cx);
                 } else {
                     self.channel_store.update(cx, |store, cx| {
@@ -205,7 +203,7 @@ impl ChannelView {
         }
     }
 
-    fn acknowledge_buffer_version(&mut self, cx: &mut ViewContext<'_, '_, ChannelView>) {
+    fn acknowledge_buffer_version(&mut self, cx: &mut ViewContext<ChannelView>) {
         self.channel_store.update(cx, |store, cx| {
             let channel_buffer = self.channel_buffer.read(cx);
             store.acknowledge_notes_version(
@@ -221,49 +219,39 @@ impl ChannelView {
     }
 }
 
-impl Entity for ChannelView {
-    type Event = editor::Event;
-}
-
-impl View for ChannelView {
-    fn ui_name() -> &'static str {
-        "ChannelView"
-    }
+impl EventEmitter<EditorEvent> for ChannelView {}
 
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        ChildView::new(self.editor.as_any(), cx).into_any()
+impl Render for ChannelView {
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
+        self.editor.clone()
     }
+}
 
-    fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
-        if cx.is_self_focused() {
-            self.acknowledge_buffer_version(cx);
-            cx.focus(self.editor.as_any())
-        }
+impl FocusableView for ChannelView {
+    fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
+        self.editor.read(cx).focus_handle(cx)
     }
 }
 
 impl Item for ChannelView {
+    type Event = EditorEvent;
+
     fn act_as_type<'a>(
         &'a self,
         type_id: TypeId,
-        self_handle: &'a ViewHandle<Self>,
+        self_handle: &'a View<Self>,
         _: &'a AppContext,
-    ) -> Option<&'a AnyViewHandle> {
+    ) -> Option<AnyView> {
         if type_id == TypeId::of::<Self>() {
-            Some(self_handle)
+            Some(self_handle.to_any())
         } else if type_id == TypeId::of::<Editor>() {
-            Some(&self.editor)
+            Some(self.editor.to_any())
         } else {
             None
         }
     }
 
-    fn tab_content<V: 'static>(
-        &self,
-        _: Option<usize>,
-        style: &theme::Tab,
-        cx: &gpui::AppContext,
-    ) -> AnyElement<V> {
+    fn tab_content(&self, _: Option<usize>, selected: bool, cx: &WindowContext) -> AnyElement {
         let label = if let Some(channel) = self.channel(cx) {
             match (
                 channel.can_edit_notes(),
@@ -276,16 +264,24 @@ impl Item for ChannelView {
         } else {
             format!("channel notes (disconnected)")
         };
-        Label::new(label, style.label.to_owned()).into_any()
+        Label::new(label)
+            .color(if selected {
+                Color::Default
+            } else {
+                Color::Muted
+            })
+            .into_any_element()
     }
 
-    fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<Self> {
-        Some(Self::new(
-            self.project.clone(),
-            self.channel_store.clone(),
-            self.channel_buffer.clone(),
-            cx,
-        ))
+    fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<View<Self>> {
+        Some(cx.new_view(|cx| {
+            Self::new(
+                self.project.clone(),
+                self.channel_store.clone(),
+                self.channel_buffer.clone(),
+                cx,
+            )
+        }))
     }
 
     fn is_singleton(&self, _cx: &AppContext) -> bool {
@@ -307,7 +303,7 @@ impl Item for ChannelView {
             .update(cx, |editor, cx| Item::set_nav_history(editor, history, cx))
     }
 
-    fn as_searchable(&self, _: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
+    fn as_searchable(&self, _: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
         Some(Box::new(self.editor.clone()))
     }
 
@@ -315,12 +311,12 @@ impl Item for ChannelView {
         true
     }
 
-    fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Vector2F> {
+    fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Point<Pixels>> {
         self.editor.read(cx).pixel_position_of_cursor(cx)
     }
 
-    fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
-        editor::Editor::to_item_events(event)
+    fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
+        Editor::to_item_events(event, f)
     }
 }
 
@@ -329,7 +325,7 @@ impl FollowableItem for ChannelView {
         self.remote_id
     }
 
-    fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
+    fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
         let channel_buffer = self.channel_buffer.read(cx);
         if !channel_buffer.is_connected() {
             return None;
@@ -350,12 +346,12 @@ impl FollowableItem for ChannelView {
     }
 
     fn from_state_proto(
-        pane: ViewHandle<workspace::Pane>,
-        workspace: ViewHandle<workspace::Workspace>,
+        pane: View<workspace::Pane>,
+        workspace: View<workspace::Workspace>,
         remote_id: workspace::ViewId,
         state: &mut Option<proto::view::Variant>,
-        cx: &mut AppContext,
-    ) -> Option<gpui::Task<anyhow::Result<ViewHandle<Self>>>> {
+        cx: &mut WindowContext,
+    ) -> Option<gpui::Task<anyhow::Result<View<Self>>>> {
         let Some(proto::view::Variant::ChannelView(_)) = state else {
             return None;
         };
@@ -368,30 +364,28 @@ impl FollowableItem for ChannelView {
         Some(cx.spawn(|mut cx| async move {
             let this = open.await?;
 
-            let task = this
-                .update(&mut cx, |this, cx| {
-                    this.remote_id = Some(remote_id);
-
-                    if let Some(state) = state.editor {
-                        Some(this.editor.update(cx, |editor, cx| {
-                            editor.apply_update_proto(
-                                &this.project,
-                                proto::update_view::Variant::Editor(proto::update_view::Editor {
-                                    selections: state.selections,
-                                    pending_selection: state.pending_selection,
-                                    scroll_top_anchor: state.scroll_top_anchor,
-                                    scroll_x: state.scroll_x,
-                                    scroll_y: state.scroll_y,
-                                    ..Default::default()
-                                }),
-                                cx,
-                            )
-                        }))
-                    } else {
-                        None
-                    }
-                })
-                .ok_or_else(|| anyhow!("window was closed"))?;
+            let task = this.update(&mut cx, |this, cx| {
+                this.remote_id = Some(remote_id);
+
+                if let Some(state) = state.editor {
+                    Some(this.editor.update(cx, |editor, cx| {
+                        editor.apply_update_proto(
+                            &this.project,
+                            proto::update_view::Variant::Editor(proto::update_view::Editor {
+                                selections: state.selections,
+                                pending_selection: state.pending_selection,
+                                scroll_top_anchor: state.scroll_top_anchor,
+                                scroll_x: state.scroll_x,
+                                scroll_y: state.scroll_y,
+                                ..Default::default()
+                            }),
+                            cx,
+                        )
+                    }))
+                } else {
+                    None
+                }
+            })?;
 
             if let Some(task) = task {
                 task.await?;
@@ -403,9 +397,9 @@ impl FollowableItem for ChannelView {
 
     fn add_event_to_update_proto(
         &self,
-        event: &Self::Event,
+        event: &EditorEvent,
         update: &mut Option<proto::update_view::Variant>,
-        cx: &AppContext,
+        cx: &WindowContext,
     ) -> bool {
         self.editor
             .read(cx)
@@ -414,7 +408,7 @@ impl FollowableItem for ChannelView {
 
     fn apply_update_proto(
         &mut self,
-        project: &ModelHandle<Project>,
+        project: &Model<Project>,
         message: proto::update_view::Variant,
         cx: &mut ViewContext<Self>,
     ) -> gpui::Task<anyhow::Result<()>> {
@@ -429,16 +423,16 @@ impl FollowableItem for ChannelView {
         })
     }
 
-    fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool {
-        Editor::should_unfollow_on_event(event, cx)
+    fn is_project_item(&self, _cx: &WindowContext) -> bool {
+        false
     }
 
-    fn is_project_item(&self, _cx: &AppContext) -> bool {
-        false
+    fn to_follow_event(event: &Self::Event) -> Option<workspace::item::FollowEvent> {
+        Editor::to_follow_event(event)
     }
 }
 
-struct ChannelBufferCollaborationHub(ModelHandle<ChannelBuffer>);
+struct ChannelBufferCollaborationHub(Model<ChannelBuffer>);
 
 impl CollaborationHub for ChannelBufferCollaborationHub {
     fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator> {

crates/collab_ui/src/chat_panel.rs 🔗

@@ -1,6 +1,4 @@
-use crate::{
-    channel_view::ChannelView, is_channels_feature_enabled, render_avatar, ChatPanelSettings,
-};
+use crate::{channel_view::ChannelView, is_channels_feature_enabled, ChatPanelSettings};
 use anyhow::Result;
 use call::ActiveCall;
 use channel::{ChannelChat, ChannelChatEvent, ChannelMessageId, ChannelStore};
@@ -9,13 +7,9 @@ use collections::HashMap;
 use db::kvp::KEY_VALUE_STORE;
 use editor::Editor;
 use gpui::{
-    actions,
-    elements::*,
-    platform::{CursorStyle, MouseButton},
-    serde_json,
-    views::{ItemType, Select, SelectStyle},
-    AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Subscription, Task, View,
-    ViewContext, ViewHandle, WeakViewHandle,
+    actions, div, list, prelude::*, px, serde_json, AnyElement, AppContext, AsyncWindowContext,
+    ClickEvent, ElementId, EventEmitter, FocusableView, ListOffset, ListScrollEvent, ListState,
+    Model, Render, Subscription, Task, View, ViewContext, VisualContext, WeakView,
 };
 use language::LanguageRegistry;
 use menu::Confirm;
@@ -23,13 +17,14 @@ use message_editor::MessageEditor;
 use project::Fs;
 use rich_text::RichText;
 use serde::{Deserialize, Serialize};
-use settings::SettingsStore;
+use settings::{Settings, SettingsStore};
 use std::sync::Arc;
-use theme::{IconButton, Theme};
+use theme::ActiveTheme as _;
 use time::{OffsetDateTime, UtcOffset};
+use ui::{prelude::*, Avatar, Button, Icon, IconButton, Label, TabBar, Tooltip};
 use util::{ResultExt, TryFutureExt};
 use workspace::{
-    dock::{DockPosition, Panel},
+    dock::{DockPosition, Panel, PanelEvent},
     Workspace,
 };
 
@@ -38,29 +33,36 @@ mod message_editor;
 const MESSAGE_LOADING_THRESHOLD: usize = 50;
 const CHAT_PANEL_KEY: &'static str = "ChatPanel";
 
+pub fn init(cx: &mut AppContext) {
+    cx.observe_new_views(|workspace: &mut Workspace, _| {
+        workspace.register_action(|workspace, _: &ToggleFocus, cx| {
+            workspace.toggle_panel_focus::<ChatPanel>(cx);
+        });
+    })
+    .detach();
+}
+
 pub struct ChatPanel {
     client: Arc<Client>,
-    channel_store: ModelHandle<ChannelStore>,
+    channel_store: Model<ChannelStore>,
     languages: Arc<LanguageRegistry>,
-    active_chat: Option<(ModelHandle<ChannelChat>, Subscription)>,
-    message_list: ListState<ChatPanel>,
-    input_editor: ViewHandle<MessageEditor>,
-    channel_select: ViewHandle<Select>,
+    message_list: ListState,
+    active_chat: Option<(Model<ChannelChat>, Subscription)>,
+    input_editor: View<MessageEditor>,
     local_timezone: UtcOffset,
     fs: Arc<dyn Fs>,
-    width: Option<f32>,
+    width: Option<Pixels>,
     active: bool,
     pending_serialization: Task<Option<()>>,
     subscriptions: Vec<gpui::Subscription>,
-    workspace: WeakViewHandle<Workspace>,
+    workspace: WeakView<Workspace>,
     is_scrolled_to_bottom: bool,
-    has_focus: bool,
     markdown_data: HashMap<ChannelMessageId, RichText>,
 }
 
 #[derive(Serialize, Deserialize)]
 struct SerializedChatPanel {
-    width: Option<f32>,
+    width: Option<Pixels>,
 }
 
 #[derive(Debug)]
@@ -70,90 +72,56 @@ pub enum Event {
     Dismissed,
 }
 
-actions!(
-    chat_panel,
-    [LoadMoreMessages, ToggleFocus, OpenChannelNotes, JoinCall]
-);
-
-pub fn init(cx: &mut AppContext) {
-    cx.add_action(ChatPanel::send);
-    cx.add_action(ChatPanel::load_more_messages);
-    cx.add_action(ChatPanel::open_notes);
-    cx.add_action(ChatPanel::join_call);
-}
+actions!(chat_panel, [ToggleFocus]);
 
 impl ChatPanel {
-    pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
+    pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
         let fs = workspace.app_state().fs.clone();
         let client = workspace.app_state().client.clone();
         let channel_store = ChannelStore::global(cx);
         let languages = workspace.app_state().languages.clone();
 
-        let input_editor = cx.add_view(|cx| {
+        let input_editor = cx.new_view(|cx| {
             MessageEditor::new(
                 languages.clone(),
                 channel_store.clone(),
-                cx.add_view(|cx| {
-                    Editor::auto_height(
-                        4,
-                        Some(Arc::new(|theme| theme.chat_panel.input_editor.clone())),
-                        cx,
-                    )
-                }),
+                cx.new_view(|cx| Editor::auto_height(4, cx)),
                 cx,
             )
         });
 
         let workspace_handle = workspace.weak_handle();
 
-        let channel_select = cx.add_view(|cx| {
-            let channel_store = channel_store.clone();
-            let workspace = workspace_handle.clone();
-            Select::new(0, cx, {
-                move |ix, item_type, is_hovered, cx| {
-                    Self::render_channel_name(
-                        &channel_store,
-                        ix,
-                        item_type,
-                        is_hovered,
-                        workspace,
-                        cx,
-                    )
-                }
-            })
-            .with_style(move |cx| {
-                let style = &theme::current(cx).chat_panel.channel_select;
-                SelectStyle {
-                    header: Default::default(),
-                    menu: style.menu,
-                }
-            })
-        });
+        cx.new_view(|cx: &mut ViewContext<Self>| {
+            let view = cx.view().downgrade();
+            let message_list =
+                ListState::new(0, gpui::ListAlignment::Bottom, px(1000.), move |ix, cx| {
+                    if let Some(view) = view.upgrade() {
+                        view.update(cx, |view, cx| {
+                            view.render_message(ix, cx).into_any_element()
+                        })
+                    } else {
+                        div().into_any()
+                    }
+                });
 
-        let mut message_list =
-            ListState::<Self>::new(0, Orientation::Bottom, 10., move |this, ix, cx| {
-                this.render_message(ix, cx)
-            });
-        message_list.set_scroll_handler(|visible_range, count, this, cx| {
-            if visible_range.start < MESSAGE_LOADING_THRESHOLD {
-                this.load_more_messages(&LoadMoreMessages, cx);
-            }
-            this.is_scrolled_to_bottom = visible_range.end == count;
-        });
+            message_list.set_scroll_handler(cx.listener(|this, event: &ListScrollEvent, cx| {
+                if event.visible_range.start < MESSAGE_LOADING_THRESHOLD {
+                    this.load_more_messages(cx);
+                }
+                this.is_scrolled_to_bottom = event.visible_range.end == event.count;
+            }));
 
-        cx.add_view(|cx| {
             let mut this = Self {
                 fs,
                 client,
                 channel_store,
                 languages,
+                message_list,
                 active_chat: Default::default(),
                 pending_serialization: Task::ready(None),
-                message_list,
                 input_editor,
-                channel_select,
-                local_timezone: cx.platform().local_timezone(),
-                has_focus: false,
+                local_timezone: cx.local_timezone(),
                 subscriptions: Vec::new(),
                 workspace: workspace_handle,
                 is_scrolled_to_bottom: true,
@@ -163,38 +131,16 @@ impl ChatPanel {
             };
 
             let mut old_dock_position = this.position(cx);
-            this.subscriptions
-                .push(
-                    cx.observe_global::<SettingsStore, _>(move |this: &mut Self, cx| {
-                        let new_dock_position = this.position(cx);
-                        if new_dock_position != old_dock_position {
-                            old_dock_position = new_dock_position;
-                            cx.emit(Event::DockPositionChanged);
-                        }
-                        cx.notify();
-                    }),
-                );
-
-            this.update_channel_count(cx);
-            cx.observe(&this.channel_store, |this, _, cx| {
-                this.update_channel_count(cx)
-            })
-            .detach();
-
-            cx.observe(&this.channel_select, |this, channel_select, cx| {
-                let selected_ix = channel_select.read(cx).selected_index();
-
-                let selected_channel_id = this
-                    .channel_store
-                    .read(cx)
-                    .channel_at(selected_ix)
-                    .map(|e| e.id);
-                if let Some(selected_channel_id) = selected_channel_id {
-                    this.select_channel(selected_channel_id, None, cx)
-                        .detach_and_log_err(cx);
-                }
-            })
-            .detach();
+            this.subscriptions.push(cx.observe_global::<SettingsStore>(
+                move |this: &mut Self, cx| {
+                    let new_dock_position = this.position(cx);
+                    if new_dock_position != old_dock_position {
+                        old_dock_position = new_dock_position;
+                        cx.emit(Event::DockPositionChanged);
+                    }
+                    cx.notify();
+                },
+            ));
 
             this
         })
@@ -204,17 +150,17 @@ impl ChatPanel {
         self.is_scrolled_to_bottom
     }
 
-    pub fn active_chat(&self) -> Option<ModelHandle<ChannelChat>> {
+    pub fn active_chat(&self) -> Option<Model<ChannelChat>> {
         self.active_chat.as_ref().map(|(chat, _)| chat.clone())
     }
 
     pub fn load(
-        workspace: WeakViewHandle<Workspace>,
-        cx: AsyncAppContext,
-    ) -> Task<Result<ViewHandle<Self>>> {
+        workspace: WeakView<Workspace>,
+        cx: AsyncWindowContext,
+    ) -> Task<Result<View<Self>>> {
         cx.spawn(|mut cx| async move {
             let serialized_panel = if let Some(panel) = cx
-                .background()
+                .background_executor()
                 .spawn(async move { KEY_VALUE_STORE.read_kvp(CHAT_PANEL_KEY) })
                 .await
                 .log_err()
@@ -240,7 +186,7 @@ impl ChatPanel {
 
     fn serialize(&mut self, cx: &mut ViewContext<Self>) {
         let width = self.width;
-        self.pending_serialization = cx.background().spawn(
+        self.pending_serialization = cx.background_executor().spawn(
             async move {
                 KEY_VALUE_STORE
                     .write_kvp(
@@ -254,14 +200,7 @@ impl ChatPanel {
         );
     }
 
-    fn update_channel_count(&mut self, cx: &mut ViewContext<Self>) {
-        let channel_count = self.channel_store.read(cx).channel_count();
-        self.channel_select.update(cx, |select, cx| {
-            select.set_item_count(channel_count, cx);
-        });
-    }
-
-    fn set_active_chat(&mut self, chat: ModelHandle<ChannelChat>, cx: &mut ViewContext<Self>) {
+    fn set_active_chat(&mut self, chat: Model<ChannelChat>, cx: &mut ViewContext<Self>) {
         if self.active_chat.as_ref().map(|e| &e.0) != Some(&chat) {
             let channel_id = chat.read(cx).channel_id;
             {
@@ -277,18 +216,13 @@ impl ChatPanel {
             let subscription = cx.subscribe(&chat, Self::channel_did_change);
             self.active_chat = Some((chat, subscription));
             self.acknowledge_last_message(cx);
-            self.channel_select.update(cx, |select, cx| {
-                if let Some(ix) = self.channel_store.read(cx).index_of_channel(channel_id) {
-                    select.set_selected_index(ix, cx);
-                }
-            });
             cx.notify();
         }
     }
 
     fn channel_did_change(
         &mut self,
-        _: ModelHandle<ChannelChat>,
+        _: Model<ChannelChat>,
         event: &ChannelChatEvent,
         cx: &mut ViewContext<Self>,
     ) {
@@ -316,7 +250,7 @@ impl ChatPanel {
         cx.notify();
     }
 
-    fn acknowledge_last_message(&mut self, cx: &mut ViewContext<'_, '_, ChatPanel>) {
+    fn acknowledge_last_message(&mut self, cx: &mut ViewContext<Self>) {
         if self.active && self.is_scrolled_to_bottom {
             if let Some((chat, _)) = &self.active_chat {
                 chat.update(cx, |chat, cx| {
@@ -326,39 +260,60 @@ impl ChatPanel {
         }
     }
 
-    fn render_channel(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let theme = theme::current(cx);
-        Flex::column()
-            .with_child(
-                ChildView::new(&self.channel_select, cx)
-                    .contained()
-                    .with_style(theme.chat_panel.channel_select.container),
+    fn render_channel(&self, cx: &mut ViewContext<Self>) -> AnyElement {
+        v_stack()
+            .full()
+            .on_action(cx.listener(Self::send))
+            .child(
+                h_stack().z_index(1).child(
+                    TabBar::new("chat_header")
+                        .child(
+                            h_stack()
+                                .w_full()
+                                .h(rems(ui::Tab::HEIGHT_IN_REMS))
+                                .px_2()
+                                .child(Label::new(
+                                    self.active_chat
+                                        .as_ref()
+                                        .and_then(|c| {
+                                            Some(format!("#{}", c.0.read(cx).channel(cx)?.name))
+                                        })
+                                        .unwrap_or_default(),
+                                )),
+                        )
+                        .end_child(
+                            IconButton::new("notes", Icon::File)
+                                .on_click(cx.listener(Self::open_notes))
+                                .tooltip(|cx| Tooltip::text("Open notes", cx)),
+                        )
+                        .end_child(
+                            IconButton::new("call", Icon::AudioOn)
+                                .on_click(cx.listener(Self::join_call))
+                                .tooltip(|cx| Tooltip::text("Join call", cx)),
+                        ),
+                ),
+            )
+            .child(div().flex_grow().px_2().py_1().map(|this| {
+                if self.active_chat.is_some() {
+                    this.child(list(self.message_list.clone()).full())
+                } else {
+                    this
+                }
+            }))
+            .child(
+                div()
+                    .z_index(1)
+                    .p_2()
+                    .bg(cx.theme().colors().background)
+                    .child(self.input_editor.clone()),
             )
-            .with_child(self.render_active_channel_messages(&theme))
-            .with_child(self.render_input_box(&theme, cx))
             .into_any()
     }
 
-    fn render_active_channel_messages(&self, theme: &Arc<Theme>) -> AnyElement<Self> {
-        let messages = if self.active_chat.is_some() {
-            List::new(self.message_list.clone())
-                .contained()
-                .with_style(theme.chat_panel.list)
-                .into_any()
-        } else {
-            Empty::new().into_any()
-        };
-
-        messages.flex(1., true).into_any()
-    }
-
-    fn render_message(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let (message, is_continuation, is_last, is_admin) = self
-            .active_chat
-            .as_ref()
-            .unwrap()
-            .0
-            .update(cx, |active_chat, cx| {
+    fn render_message(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> impl IntoElement {
+        let active_chat = &self.active_chat.as_ref().unwrap().0;
+        let (message, is_continuation_from_previous, is_continuation_to_next, is_admin) =
+            active_chat.update(cx, |active_chat, cx| {
                 let is_admin = self
                     .channel_store
                     .read(cx)
@@ -366,8 +321,13 @@ impl ChatPanel {
 
                 let last_message = active_chat.message(ix.saturating_sub(1));
                 let this_message = active_chat.message(ix).clone();
-                let is_continuation = last_message.id != this_message.id
-                    && this_message.sender.id == last_message.sender.id;
+                let next_message =
+                    active_chat.message(ix.saturating_add(1).min(active_chat.message_count() - 1));
+
+                let is_continuation_from_previous = last_message.id != this_message.id
+                    && last_message.sender.id == this_message.sender.id;
+                let is_continuation_to_next = this_message.id != next_message.id
+                    && this_message.sender.id == next_message.sender.id;
 
                 if let ChannelMessageId::Saved(id) = this_message.id {
                     if this_message
@@ -381,28 +341,19 @@ impl ChatPanel {
 
                 (
                     this_message,
-                    is_continuation,
-                    active_chat.message_count() == ix + 1,
+                    is_continuation_from_previous,
+                    is_continuation_to_next,
                     is_admin,
                 )
             });
 
-        let is_pending = message.is_pending();
-        let theme = theme::current(cx);
+        let _is_pending = message.is_pending();
         let text = self.markdown_data.entry(message.id).or_insert_with(|| {
             Self::render_markdown_with_mentions(&self.languages, self.client.id(), &message)
         });
 
         let now = OffsetDateTime::now_utc();
 
-        let style = if is_pending {
-            &theme.chat_panel.pending_message
-        } else if is_continuation {
-            &theme.chat_panel.continuation_message
-        } else {
-            &theme.chat_panel.message
-        };
-
         let belongs_to_user = Some(message.sender.id) == self.client.user_id();
         let message_id_to_remove = if let (ChannelMessageId::Saved(id), true) =
             (message.id, belongs_to_user || is_admin)
@@ -412,89 +363,52 @@ impl ChatPanel {
             None
         };
 
-        enum MessageBackgroundHighlight {}
-        MouseEventHandler::new::<MessageBackgroundHighlight, _>(ix, cx, |state, cx| {
-            let container = style.style_for(state);
-            if is_continuation {
-                Flex::row()
-                    .with_child(
-                        text.element(
-                            theme.editor.syntax.clone(),
-                            theme.chat_panel.rich_text.clone(),
-                            cx,
+        let element_id: ElementId = match message.id {
+            ChannelMessageId::Saved(id) => ("saved-message", id).into(),
+            ChannelMessageId::Pending(id) => ("pending-message", id).into(),
+        };
+
+        v_stack()
+            .w_full()
+            .id(element_id)
+            .relative()
+            .overflow_hidden()
+            .group("")
+            .when(!is_continuation_from_previous, |this| {
+                this.child(
+                    h_stack()
+                        .gap_2()
+                        .child(Avatar::new(message.sender.avatar_uri.clone()))
+                        .child(Label::new(message.sender.github_login.clone()))
+                        .child(
+                            Label::new(format_timestamp(
+                                message.timestamp,
+                                now,
+                                self.local_timezone,
+                            ))
+                            .color(Color::Muted),
+                        ),
+                )
+            })
+            .when(!is_continuation_to_next, |this|
+                // HACK: This should really be a margin, but margins seem to get collapsed.
+                this.pb_2())
+            .child(text.element("body".into(), cx))
+            .child(
+                div()
+                    .absolute()
+                    .top_1()
+                    .right_2()
+                    .w_8()
+                    .visible_on_hover("")
+                    .children(message_id_to_remove.map(|message_id| {
+                        IconButton::new(("remove", message_id), Icon::XCircle).on_click(
+                            cx.listener(move |this, _, cx| {
+                                this.remove_message(message_id, cx);
+                            }),
                         )
-                        .flex(1., true),
-                    )
-                    .with_child(render_remove(message_id_to_remove, cx, &theme))
-                    .contained()
-                    .with_style(*container)
-                    .with_margin_bottom(if is_last {
-                        theme.chat_panel.last_message_bottom_spacing
-                    } else {
-                        0.
-                    })
-                    .into_any()
-            } else {
-                Flex::column()
-                    .with_child(
-                        Flex::row()
-                            .with_child(
-                                Flex::row()
-                                    .with_child(render_avatar(
-                                        message.sender.avatar.clone(),
-                                        &theme.chat_panel.avatar,
-                                        theme.chat_panel.avatar_container,
-                                    ))
-                                    .with_child(
-                                        Label::new(
-                                            message.sender.github_login.clone(),
-                                            theme.chat_panel.message_sender.text.clone(),
-                                        )
-                                        .contained()
-                                        .with_style(theme.chat_panel.message_sender.container),
-                                    )
-                                    .with_child(
-                                        Label::new(
-                                            format_timestamp(
-                                                message.timestamp,
-                                                now,
-                                                self.local_timezone,
-                                            ),
-                                            theme.chat_panel.message_timestamp.text.clone(),
-                                        )
-                                        .contained()
-                                        .with_style(theme.chat_panel.message_timestamp.container),
-                                    )
-                                    .align_children_center()
-                                    .flex(1., true),
-                            )
-                            .with_child(render_remove(message_id_to_remove, cx, &theme))
-                            .align_children_center(),
-                    )
-                    .with_child(
-                        Flex::row()
-                            .with_child(
-                                text.element(
-                                    theme.editor.syntax.clone(),
-                                    theme.chat_panel.rich_text.clone(),
-                                    cx,
-                                )
-                                .flex(1., true),
-                            )
-                            // Add a spacer to make everything line up
-                            .with_child(render_remove(None, cx, &theme)),
-                    )
-                    .contained()
-                    .with_style(*container)
-                    .with_margin_bottom(if is_last {
-                        theme.chat_panel.last_message_bottom_spacing
-                    } else {
-                        0.
-                    })
-                    .into_any()
-            }
-        })
-        .into_any()
+                    })),
+            )
     }
 
     fn render_markdown_with_mentions(
@@ -514,127 +428,26 @@ impl ChatPanel {
         rich_text::render_markdown(message.body.clone(), &mentions, language_registry, None)
     }
 
-    fn render_input_box(&self, theme: &Arc<Theme>, cx: &AppContext) -> AnyElement<Self> {
-        ChildView::new(&self.input_editor, cx)
-            .contained()
-            .with_style(theme.chat_panel.input_editor.container)
-            .into_any()
-    }
-
-    fn render_channel_name(
-        channel_store: &ModelHandle<ChannelStore>,
-        ix: usize,
-        item_type: ItemType,
-        is_hovered: bool,
-        workspace: WeakViewHandle<Workspace>,
-        cx: &mut ViewContext<Select>,
-    ) -> AnyElement<Select> {
-        let theme = theme::current(cx);
-        let tooltip_style = &theme.tooltip;
-        let theme = &theme.chat_panel;
-        let style = match (&item_type, is_hovered) {
-            (ItemType::Header, _) => &theme.channel_select.header,
-            (ItemType::Selected, _) => &theme.channel_select.active_item,
-            (ItemType::Unselected, false) => &theme.channel_select.item,
-            (ItemType::Unselected, true) => &theme.channel_select.hovered_item,
-        };
-
-        let channel = &channel_store.read(cx).channel_at(ix).unwrap();
-        let channel_id = channel.id;
-
-        let mut row = Flex::row()
-            .with_child(
-                Label::new("#".to_string(), style.hash.text.clone())
-                    .contained()
-                    .with_style(style.hash.container),
-            )
-            .with_child(Label::new(channel.name.clone(), style.name.clone()));
-
-        if matches!(item_type, ItemType::Header) {
-            row.add_children([
-                MouseEventHandler::new::<OpenChannelNotes, _>(0, cx, |mouse_state, _| {
-                    render_icon_button(theme.icon_button.style_for(mouse_state), "icons/file.svg")
-                })
-                .on_click(MouseButton::Left, move |_, _, cx| {
-                    if let Some(workspace) = workspace.upgrade(cx) {
-                        ChannelView::open(channel_id, workspace, cx).detach();
+    fn render_sign_in_prompt(&self, cx: &mut ViewContext<Self>) -> AnyElement {
+        Button::new("sign-in", "Sign in to use chat")
+            .on_click(cx.listener(move |this, _, cx| {
+                let client = this.client.clone();
+                cx.spawn(|this, mut cx| async move {
+                    if client
+                        .authenticate_and_connect(true, &cx)
+                        .log_err()
+                        .await
+                        .is_some()
+                    {
+                        this.update(&mut cx, |_, cx| {
+                            cx.focus_self();
+                        })
+                        .ok();
                     }
                 })
-                .with_tooltip::<OpenChannelNotes>(
-                    channel_id as usize,
-                    "Open Notes",
-                    Some(Box::new(OpenChannelNotes)),
-                    tooltip_style.clone(),
-                    cx,
-                )
-                .flex_float(),
-                MouseEventHandler::new::<ActiveCall, _>(0, cx, |mouse_state, _| {
-                    render_icon_button(
-                        theme.icon_button.style_for(mouse_state),
-                        "icons/speaker-loud.svg",
-                    )
-                })
-                .on_click(MouseButton::Left, move |_, _, cx| {
-                    ActiveCall::global(cx)
-                        .update(cx, |call, cx| call.join_channel(channel_id, cx))
-                        .detach_and_log_err(cx);
-                })
-                .with_tooltip::<ActiveCall>(
-                    channel_id as usize,
-                    "Join Call",
-                    Some(Box::new(JoinCall)),
-                    tooltip_style.clone(),
-                    cx,
-                )
-                .flex_float(),
-            ]);
-        }
-
-        row.align_children_center()
-            .contained()
-            .with_style(style.container)
-            .into_any()
-    }
-
-    fn render_sign_in_prompt(
-        &self,
-        theme: &Arc<Theme>,
-        cx: &mut ViewContext<Self>,
-    ) -> AnyElement<Self> {
-        enum SignInPromptLabel {}
-
-        MouseEventHandler::new::<SignInPromptLabel, _>(0, cx, |mouse_state, _| {
-            Label::new(
-                "Sign in to use chat".to_string(),
-                theme
-                    .chat_panel
-                    .sign_in_prompt
-                    .style_for(mouse_state)
-                    .clone(),
-            )
-        })
-        .with_cursor_style(CursorStyle::PointingHand)
-        .on_click(MouseButton::Left, move |_, this, cx| {
-            let client = this.client.clone();
-            cx.spawn(|this, mut cx| async move {
-                if client
-                    .authenticate_and_connect(true, &cx)
-                    .log_err()
-                    .await
-                    .is_some()
-                {
-                    this.update(&mut cx, |this, cx| {
-                        if cx.handle().is_focused(cx) {
-                            cx.focus(&this.input_editor);
-                        }
-                    })
-                    .ok();
-                }
-            })
-            .detach();
-        })
-        .aligned()
-        .into_any()
+                .detach();
+            }))
+            .into_any_element()
     }
 
     fn send(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
@@ -658,7 +471,7 @@ impl ChatPanel {
         }
     }
 
-    fn load_more_messages(&mut self, _: &LoadMoreMessages, cx: &mut ViewContext<Self>) {
+    fn load_more_messages(&mut self, cx: &mut ViewContext<Self>) {
         if let Some((chat, _)) = self.active_chat.as_ref() {
             chat.update(cx, |channel, cx| {
                 if let Some(task) = channel.load_more_messages(cx) {
@@ -695,14 +508,14 @@ impl ChatPanel {
 
             if let Some(message_id) = scroll_to_message_id {
                 if let Some(item_ix) =
-                    ChannelChat::load_history_since_message(chat.clone(), message_id, cx.clone())
+                    ChannelChat::load_history_since_message(chat.clone(), message_id, (*cx).clone())
                         .await
                 {
                     this.update(&mut cx, |this, cx| {
                         if this.active_chat.as_ref().map_or(false, |(c, _)| *c == chat) {
                             this.message_list.scroll_to(ListOffset {
                                 item_ix,
-                                offset_in_item: 0.,
+                                offset_in_item: px(0.0),
                             });
                             cx.notify();
                         }
@@ -714,16 +527,16 @@ impl ChatPanel {
         })
     }
 
-    fn open_notes(&mut self, _: &OpenChannelNotes, cx: &mut ViewContext<Self>) {
+    fn open_notes(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
         if let Some((chat, _)) = &self.active_chat {
             let channel_id = chat.read(cx).channel_id;
-            if let Some(workspace) = self.workspace.upgrade(cx) {
+            if let Some(workspace) = self.workspace.upgrade() {
                 ChannelView::open(channel_id, workspace, cx).detach();
             }
         }
     }
 
-    fn join_call(&mut self, _: &JoinCall, cx: &mut ViewContext<Self>) {
+    fn join_call(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
         if let Some((chat, _)) = &self.active_chat {
             let channel_id = chat.read(cx).channel_id;
             ActiveCall::global(cx)
@@ -733,89 +546,30 @@ impl ChatPanel {
     }
 }
 
-fn render_remove(
-    message_id_to_remove: Option<u64>,
-    cx: &mut ViewContext<'_, '_, ChatPanel>,
-    theme: &Arc<Theme>,
-) -> AnyElement<ChatPanel> {
-    enum DeleteMessage {}
-
-    message_id_to_remove
-        .map(|id| {
-            MouseEventHandler::new::<DeleteMessage, _>(id as usize, cx, |mouse_state, _| {
-                let button_style = theme.chat_panel.icon_button.style_for(mouse_state);
-                render_icon_button(button_style, "icons/x.svg")
-                    .aligned()
-                    .into_any()
-            })
-            .with_padding(Padding::uniform(2.))
-            .with_cursor_style(CursorStyle::PointingHand)
-            .on_click(MouseButton::Left, move |_, this, cx| {
-                this.remove_message(id, cx);
-            })
-            .flex_float()
-            .into_any()
-        })
-        .unwrap_or_else(|| {
-            let style = theme.chat_panel.icon_button.default;
-
-            Empty::new()
-                .constrained()
-                .with_width(style.icon_width)
-                .aligned()
-                .constrained()
-                .with_width(style.button_width)
-                .with_height(style.button_width)
-                .contained()
-                .with_uniform_padding(2.)
-                .flex_float()
-                .into_any()
-        })
-}
-
-impl Entity for ChatPanel {
-    type Event = Event;
-}
+impl EventEmitter<Event> for ChatPanel {}
 
-impl View for ChatPanel {
-    fn ui_name() -> &'static str {
-        "ChatPanel"
-    }
-
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let theme = theme::current(cx);
-        let element = if self.client.user_id().is_some() {
-            self.render_channel(cx)
-        } else {
-            self.render_sign_in_prompt(&theme, cx)
-        };
-        element
-            .contained()
-            .with_style(theme.chat_panel.container)
-            .constrained()
-            .with_min_width(150.)
-            .into_any()
-    }
-
-    fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
-        self.has_focus = true;
-        if matches!(
-            *self.client.status().borrow(),
-            client::Status::Connected { .. }
-        ) {
-            let editor = self.input_editor.read(cx).editor.clone();
-            cx.focus(&editor);
-        }
+impl Render for ChatPanel {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+        div()
+            .full()
+            .child(if self.client.user_id().is_some() {
+                self.render_channel(cx)
+            } else {
+                self.render_sign_in_prompt(cx)
+            })
+            .min_w(px(150.))
     }
+}
 
-    fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
-        self.has_focus = false;
+impl FocusableView for ChatPanel {
+    fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
+        self.input_editor.read(cx).focus_handle(cx)
     }
 }
 
 impl Panel for ChatPanel {
     fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
-        settings::get::<ChatPanelSettings>(cx).dock
+        ChatPanelSettings::get_global(cx).dock
     }
 
     fn position_is_valid(&self, position: DockPosition) -> bool {
@@ -828,12 +582,12 @@ impl Panel for ChatPanel {
         });
     }
 
-    fn size(&self, cx: &gpui::WindowContext) -> f32 {
+    fn size(&self, cx: &gpui::WindowContext) -> Pixels {
         self.width
-            .unwrap_or_else(|| settings::get::<ChatPanelSettings>(cx).default_width)
+            .unwrap_or_else(|| ChatPanelSettings::get_global(cx).default_width)
     }
 
-    fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
+    fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
         self.width = size;
         self.serialize(cx);
         cx.notify();
@@ -849,32 +603,25 @@ impl Panel for ChatPanel {
         }
     }
 
-    fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
-        (settings::get::<ChatPanelSettings>(cx).button && is_channels_feature_enabled(cx))
-            .then(|| "icons/conversations.svg")
-    }
-
-    fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
-        ("Chat Panel".to_string(), Some(Box::new(ToggleFocus)))
-    }
-
-    fn should_change_position_on_event(event: &Self::Event) -> bool {
-        matches!(event, Event::DockPositionChanged)
+    fn persistent_name() -> &'static str {
+        "ChatPanel"
     }
 
-    fn should_close_on_event(event: &Self::Event) -> bool {
-        matches!(event, Event::Dismissed)
+    fn icon(&self, _cx: &WindowContext) -> Option<ui::Icon> {
+        Some(ui::Icon::MessageBubbles)
     }
 
-    fn has_focus(&self, _cx: &gpui::WindowContext) -> bool {
-        self.has_focus
+    fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
+        Some("Chat Panel")
     }
 
-    fn is_focus_event(event: &Self::Event) -> bool {
-        matches!(event, Event::Focus)
+    fn toggle_action(&self) -> Box<dyn gpui::Action> {
+        Box::new(ToggleFocus)
     }
 }
 
+impl EventEmitter<PanelEvent> for ChatPanel {}
+
 fn format_timestamp(
     mut timestamp: OffsetDateTime,
     mut now: OffsetDateTime,
@@ -900,25 +647,12 @@ fn format_timestamp(
     }
 }
 
-fn render_icon_button<V: View>(style: &IconButton, svg_path: &'static str) -> impl Element<V> {
-    Svg::new(svg_path)
-        .with_color(style.color)
-        .constrained()
-        .with_width(style.icon_width)
-        .aligned()
-        .constrained()
-        .with_width(style.button_width)
-        .with_height(style.button_width)
-        .contained()
-        .with_style(style.container)
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;
-    use gpui::fonts::HighlightStyle;
+    use gpui::HighlightStyle;
     use pretty_assertions::assert_eq;
-    use rich_text::{BackgroundKind, Highlight, RenderedRegion};
+    use rich_text::Highlight;
     use util::test::marked_text_ranges;
 
     #[gpui::test]
@@ -931,7 +665,7 @@ mod tests {
             timestamp: OffsetDateTime::now_utc(),
             sender: Arc::new(client::User {
                 github_login: "fgh".into(),
-                avatar: None,
+                avatar_uri: "avatar_fgh".into(),
                 id: 103,
             }),
             nonce: 5,
@@ -949,7 +683,7 @@ mod tests {
                 (
                     ranges[0].clone(),
                     HighlightStyle {
-                        italic: Some(true),
+                        font_style: Some(gpui::FontStyle::Italic),
                         ..Default::default()
                     }
                     .into()

crates/collab_ui/src/chat_panel/message_editor.rs 🔗

@@ -3,13 +3,14 @@ use client::UserId;
 use collections::HashMap;
 use editor::{AnchorRangeExt, Editor};
 use gpui::{
-    elements::ChildView, AnyElement, AsyncAppContext, Element, Entity, ModelHandle, Task, View,
-    ViewContext, ViewHandle, WeakViewHandle,
+    AsyncWindowContext, FocusableView, IntoElement, Model, Render, SharedString, Task, View,
+    ViewContext, WeakView,
 };
 use language::{language_settings::SoftWrap, Buffer, BufferSnapshot, LanguageRegistry};
 use lazy_static::lazy_static;
 use project::search::SearchQuery;
 use std::{sync::Arc, time::Duration};
+use workspace::item::ItemHandle;
 
 const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50);
 
@@ -19,8 +20,8 @@ lazy_static! {
 }
 
 pub struct MessageEditor {
-    pub editor: ViewHandle<Editor>,
-    channel_store: ModelHandle<ChannelStore>,
+    pub editor: View<Editor>,
+    channel_store: Model<ChannelStore>,
     users: HashMap<String, UserId>,
     mentions: Vec<UserId>,
     mentions_task: Option<Task<()>>,
@@ -30,8 +31,8 @@ pub struct MessageEditor {
 impl MessageEditor {
     pub fn new(
         language_registry: Arc<LanguageRegistry>,
-        channel_store: ModelHandle<ChannelStore>,
-        editor: ViewHandle<Editor>,
+        channel_store: Model<ChannelStore>,
+        editor: View<Editor>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
         editor.update(cx, |editor, cx| {
@@ -48,15 +49,13 @@ impl MessageEditor {
         cx.subscribe(&buffer, Self::on_buffer_event).detach();
 
         let markdown = language_registry.language_for_name("Markdown");
-        cx.app_context()
-            .spawn(|mut cx| async move {
-                let markdown = markdown.await?;
-                buffer.update(&mut cx, |buffer, cx| {
-                    buffer.set_language(Some(markdown), cx)
-                });
-                anyhow::Ok(())
+        cx.spawn(|_, mut cx| async move {
+            let markdown = markdown.await?;
+            buffer.update(&mut cx, |buffer, cx| {
+                buffer.set_language(Some(markdown), cx)
             })
-            .detach_and_log_err(cx);
+        })
+        .detach_and_log_err(cx);
 
         Self {
             editor,
@@ -71,7 +70,7 @@ impl MessageEditor {
     pub fn set_channel(
         &mut self,
         channel_id: u64,
-        channel_name: Option<String>,
+        channel_name: Option<SharedString>,
         cx: &mut ViewContext<Self>,
     ) {
         self.editor.update(cx, |editor, cx| {
@@ -132,26 +131,28 @@ impl MessageEditor {
 
     fn on_buffer_event(
         &mut self,
-        buffer: ModelHandle<Buffer>,
+        buffer: Model<Buffer>,
         event: &language::Event,
         cx: &mut ViewContext<Self>,
     ) {
         if let language::Event::Reparsed | language::Event::Edited = event {
             let buffer = buffer.read(cx).snapshot();
             self.mentions_task = Some(cx.spawn(|this, cx| async move {
-                cx.background().timer(MENTIONS_DEBOUNCE_INTERVAL).await;
+                cx.background_executor()
+                    .timer(MENTIONS_DEBOUNCE_INTERVAL)
+                    .await;
                 Self::find_mentions(this, buffer, cx).await;
             }));
         }
     }
 
     async fn find_mentions(
-        this: WeakViewHandle<MessageEditor>,
+        this: WeakView<MessageEditor>,
         buffer: BufferSnapshot,
-        mut cx: AsyncAppContext,
+        mut cx: AsyncWindowContext,
     ) {
         let (buffer, ranges) = cx
-            .background()
+            .background_executor()
             .spawn(async move {
                 let ranges = MENTIONS_SEARCH.search(&buffer, None).await;
                 (buffer, ranges)
@@ -180,11 +181,7 @@ impl MessageEditor {
                 }
 
                 editor.clear_highlights::<Self>(cx);
-                editor.highlight_text::<Self>(
-                    anchor_ranges,
-                    theme::current(cx).chat_panel.rich_text.mention_highlight,
-                    cx,
-                )
+                editor.highlight_text::<Self>(anchor_ranges, gpui::red().into(), cx)
             });
 
             this.mentions = mentioned_user_ids;
@@ -192,21 +189,15 @@ impl MessageEditor {
         })
         .ok();
     }
-}
-
-impl Entity for MessageEditor {
-    type Event = ();
-}
 
-impl View for MessageEditor {
-    fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> AnyElement<Self> {
-        ChildView::new(&self.editor, cx).into_any()
+    pub(crate) fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle {
+        self.editor.read(cx).focus_handle(cx)
     }
+}
 
-    fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
-        if cx.is_self_focused() {
-            cx.focus(&self.editor);
-        }
+impl Render for MessageEditor {
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
+        self.editor.to_any()
     }
 }
 
@@ -214,7 +205,7 @@ impl View for MessageEditor {
 mod tests {
     use super::*;
     use client::{Client, User, UserStore};
-    use gpui::{TestAppContext, WindowHandle};
+    use gpui::{Context as _, TestAppContext, VisualContext as _};
     use language::{Language, LanguageConfig};
     use rpc::proto;
     use settings::SettingsStore;
@@ -222,8 +213,17 @@ mod tests {
 
     #[gpui::test]
     async fn test_message_editor(cx: &mut TestAppContext) {
-        let editor = init_test(cx);
-        let editor = editor.root(cx);
+        let language_registry = init_test(cx);
+
+        let (editor, cx) = cx.add_window_view(|cx| {
+            MessageEditor::new(
+                language_registry,
+                ChannelStore::global(cx),
+                cx.new_view(|cx| Editor::auto_height(4, cx)),
+                cx,
+            )
+        });
+        cx.executor().run_until_parked();
 
         editor.update(cx, |editor, cx| {
             editor.set_members(
@@ -232,7 +232,7 @@ mod tests {
                         user: Arc::new(User {
                             github_login: "a-b".into(),
                             id: 101,
-                            avatar: None,
+                            avatar_uri: "avatar_a-b".into(),
                         }),
                         kind: proto::channel_member::Kind::Member,
                         role: proto::ChannelRole::Member,
@@ -241,7 +241,7 @@ mod tests {
                         user: Arc::new(User {
                             github_login: "C_D".into(),
                             id: 102,
-                            avatar: None,
+                            avatar_uri: "avatar_C_D".into(),
                         }),
                         kind: proto::channel_member::Kind::Member,
                         role: proto::ChannelRole::Member,
@@ -255,7 +255,7 @@ mod tests {
             });
         });
 
-        cx.foreground().advance_clock(MENTIONS_DEBOUNCE_INTERVAL);
+        cx.executor().advance_clock(MENTIONS_DEBOUNCE_INTERVAL);
 
         editor.update(cx, |editor, cx| {
             let (text, ranges) = marked_text_ranges("Hello, «@a-b»! Have you met «@C_D»?", false);
@@ -269,15 +269,14 @@ mod tests {
         });
     }
 
-    fn init_test(cx: &mut TestAppContext) -> WindowHandle<MessageEditor> {
-        cx.foreground().forbid_parking();
-
+    fn init_test(cx: &mut TestAppContext) -> Arc<LanguageRegistry> {
         cx.update(|cx| {
             let http = FakeHttpClient::with_404_response();
             let client = Client::new(http.clone(), cx);
-            let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
-            cx.set_global(SettingsStore::test(cx));
-            theme::init((), cx);
+            let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
+            let settings = SettingsStore::test(cx);
+            cx.set_global(settings);
+            theme::init(theme::LoadThemes::JustBase, cx);
             language::init(cx);
             editor::init(cx);
             client::init(&client, cx);
@@ -292,16 +291,6 @@ mod tests {
             },
             Some(tree_sitter_markdown::language()),
         )));
-
-        let editor = cx.add_window(|cx| {
-            MessageEditor::new(
-                language_registry,
-                ChannelStore::global(cx),
-                cx.add_view(|cx| Editor::auto_height(4, None, cx)),
-                cx,
-            )
-        });
-        cx.foreground().run_until_parked();
-        editor
+        language_registry
     }
 }

crates/collab_ui/src/collab_panel.rs 🔗

@@ -1,123 +1,46 @@
 mod channel_modal;
 mod contact_finder;
 
+use self::channel_modal::ChannelModal;
 use crate::{
-    channel_view::{self, ChannelView},
-    chat_panel::ChatPanel,
-    face_pile::FacePile,
-    panel_settings, CollaborationPanelSettings,
+    channel_view::ChannelView, chat_panel::ChatPanel, face_pile::FacePile,
+    CollaborationPanelSettings,
 };
-use anyhow::Result;
 use call::ActiveCall;
 use channel::{Channel, ChannelEvent, ChannelId, ChannelStore};
-use channel_modal::ChannelModal;
-use client::{
-    proto::{self, PeerId},
-    Client, Contact, User, UserStore,
-};
+use client::{Client, Contact, User, UserStore};
 use contact_finder::ContactFinder;
-use context_menu::{ContextMenu, ContextMenuItem};
 use db::kvp::KEY_VALUE_STORE;
-use drag_and_drop::{DragAndDrop, Draggable};
-use editor::{Cancel, Editor};
+use editor::Editor;
 use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
-use futures::StreamExt;
 use fuzzy::{match_strings, StringMatchCandidate};
 use gpui::{
-    actions,
-    elements::{
-        Canvas, ChildView, Component, ContainerStyle, Empty, Flex, Image, Label, List, ListOffset,
-        ListState, MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement,
-        SafeStylable, Stack, Svg,
-    },
-    fonts::TextStyle,
-    geometry::{
-        rect::RectF,
-        vector::{vec2f, Vector2F},
-    },
-    impl_actions,
-    platform::{CursorStyle, MouseButton, PromptLevel},
-    serde_json, AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, FontCache,
-    ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
+    actions, canvas, div, fill, list, overlay, point, prelude::*, px, serde_json, AnyElement,
+    AppContext, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div, EventEmitter,
+    FocusHandle, FocusableView, InteractiveElement, IntoElement, ListOffset, ListState, Model,
+    MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, RenderOnce, SharedString,
+    Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView,
 };
-use menu::{Confirm, SelectNext, SelectPrev};
+use menu::{Cancel, Confirm, SelectNext, SelectPrev};
 use project::{Fs, Project};
+use rpc::proto::{self, PeerId};
 use serde_derive::{Deserialize, Serialize};
-use settings::SettingsStore;
-use std::{borrow::Cow, hash::Hash, mem, sync::Arc};
-use theme::{components::ComponentExt, IconButton, Interactive};
+use settings::{Settings, SettingsStore};
+use smallvec::SmallVec;
+use std::{mem, sync::Arc};
+use theme::{ActiveTheme, ThemeSettings};
+use ui::prelude::*;
+use ui::{
+    h_stack, v_stack, Avatar, Button, Color, ContextMenu, Icon, IconButton, IconElement, IconSize,
+    Label, ListHeader, ListItem, Tooltip,
+};
 use util::{maybe, ResultExt, TryFutureExt};
 use workspace::{
-    dock::{DockPosition, Panel},
-    item::ItemHandle,
-    FollowNextCollaborator, Workspace,
+    dock::{DockPosition, Panel, PanelEvent},
+    notifications::NotifyResultExt,
+    Workspace,
 };
 
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct ToggleCollapse {
-    location: ChannelId,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct NewChannel {
-    location: ChannelId,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct RenameChannel {
-    channel_id: ChannelId,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct ToggleSelectedIx {
-    ix: usize,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct RemoveChannel {
-    channel_id: ChannelId,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct InviteMembers {
-    channel_id: ChannelId,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct ManageMembers {
-    channel_id: ChannelId,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-pub struct OpenChannelNotes {
-    pub channel_id: ChannelId,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-pub struct JoinChannelCall {
-    pub channel_id: u64,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-pub struct JoinChannelChat {
-    pub channel_id: u64,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-pub struct CopyChannelLink {
-    pub channel_id: u64,
-}
-
-#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct StartMoveChannelFor {
-    channel_id: ChannelId,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
-struct MoveChannel {
-    to: ChannelId,
-}
-
 actions!(
     collab_panel,
     [
@@ -132,25 +55,6 @@ actions!(
     ]
 );
 
-impl_actions!(
-    collab_panel,
-    [
-        RemoveChannel,
-        NewChannel,
-        InviteMembers,
-        ManageMembers,
-        RenameChannel,
-        ToggleCollapse,
-        OpenChannelNotes,
-        JoinChannelCall,
-        JoinChannelChat,
-        CopyChannelLink,
-        StartMoveChannelFor,
-        MoveChannel,
-        ToggleSelectedIx
-    ]
-);
-
 #[derive(Debug, Copy, Clone, PartialEq, Eq)]
 struct ChannelMoveClipboard {
     channel_id: ChannelId,
@@ -159,90 +63,12 @@ struct ChannelMoveClipboard {
 const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel";
 
 pub fn init(cx: &mut AppContext) {
-    settings::register::<panel_settings::CollaborationPanelSettings>(cx);
-    contact_finder::init(cx);
-    channel_modal::init(cx);
-    channel_view::init(cx);
-
-    cx.add_action(CollabPanel::cancel);
-    cx.add_action(CollabPanel::select_next);
-    cx.add_action(CollabPanel::select_prev);
-    cx.add_action(CollabPanel::confirm);
-    cx.add_action(CollabPanel::insert_space);
-    cx.add_action(CollabPanel::remove);
-    cx.add_action(CollabPanel::remove_selected_channel);
-    cx.add_action(CollabPanel::show_inline_context_menu);
-    cx.add_action(CollabPanel::new_subchannel);
-    cx.add_action(CollabPanel::invite_members);
-    cx.add_action(CollabPanel::manage_members);
-    cx.add_action(CollabPanel::rename_selected_channel);
-    cx.add_action(CollabPanel::rename_channel);
-    cx.add_action(CollabPanel::toggle_channel_collapsed_action);
-    cx.add_action(CollabPanel::collapse_selected_channel);
-    cx.add_action(CollabPanel::expand_selected_channel);
-    cx.add_action(CollabPanel::open_channel_notes);
-    cx.add_action(CollabPanel::join_channel_chat);
-    cx.add_action(CollabPanel::copy_channel_link);
-
-    cx.add_action(
-        |panel: &mut CollabPanel, action: &ToggleSelectedIx, cx: &mut ViewContext<CollabPanel>| {
-            if panel.selection.take() != Some(action.ix) {
-                panel.selection = Some(action.ix)
-            }
-
-            cx.notify();
-        },
-    );
-
-    cx.add_action(
-        |panel: &mut CollabPanel,
-         action: &StartMoveChannelFor,
-         _: &mut ViewContext<CollabPanel>| {
-            panel.channel_clipboard = Some(ChannelMoveClipboard {
-                channel_id: action.channel_id,
-            });
-        },
-    );
-
-    cx.add_action(
-        |panel: &mut CollabPanel, _: &StartMoveChannel, _: &mut ViewContext<CollabPanel>| {
-            if let Some(channel) = panel.selected_channel() {
-                panel.channel_clipboard = Some(ChannelMoveClipboard {
-                    channel_id: channel.id,
-                })
-            }
-        },
-    );
-
-    cx.add_action(
-        |panel: &mut CollabPanel, _: &MoveSelected, cx: &mut ViewContext<CollabPanel>| {
-            let Some(clipboard) = panel.channel_clipboard.take() else {
-                return;
-            };
-            let Some(selected_channel) = panel.selected_channel() else {
-                return;
-            };
-
-            panel
-                .channel_store
-                .update(cx, |channel_store, cx| {
-                    channel_store.move_channel(clipboard.channel_id, Some(selected_channel.id), cx)
-                })
-                .detach_and_log_err(cx)
-        },
-    );
-
-    cx.add_action(
-        |panel: &mut CollabPanel, action: &MoveChannel, cx: &mut ViewContext<CollabPanel>| {
-            if let Some(clipboard) = panel.channel_clipboard.take() {
-                panel.channel_store.update(cx, |channel_store, cx| {
-                    channel_store
-                        .move_channel(clipboard.channel_id, Some(action.to), cx)
-                        .detach_and_log_err(cx)
-                })
-            }
-        },
-    );
+    cx.observe_new_views(|workspace: &mut Workspace, _| {
+        workspace.register_action(|workspace, _: &ToggleFocus, cx| {
+            workspace.toggle_panel_focus::<CollabPanel>(cx);
+        });
+    })
+    .detach();
 }
 
 #[derive(Debug)]
@@ -258,58 +84,42 @@ pub enum ChannelEditingState {
 }
 
 impl ChannelEditingState {
-    fn pending_name(&self) -> Option<&str> {
+    fn pending_name(&self) -> Option<String> {
         match self {
-            ChannelEditingState::Create { pending_name, .. } => pending_name.as_deref(),
-            ChannelEditingState::Rename { pending_name, .. } => pending_name.as_deref(),
+            ChannelEditingState::Create { pending_name, .. } => pending_name.clone(),
+            ChannelEditingState::Rename { pending_name, .. } => pending_name.clone(),
         }
     }
 }
 
 pub struct CollabPanel {
-    width: Option<f32>,
+    width: Option<Pixels>,
     fs: Arc<dyn Fs>,
-    has_focus: bool,
+    focus_handle: FocusHandle,
     channel_clipboard: Option<ChannelMoveClipboard>,
     pending_serialization: Task<Option<()>>,
-    context_menu: ViewHandle<ContextMenu>,
-    filter_editor: ViewHandle<Editor>,
-    channel_name_editor: ViewHandle<Editor>,
+    context_menu: Option<(View<ContextMenu>, Point<Pixels>, Subscription)>,
+    list_state: ListState,
+    filter_editor: View<Editor>,
+    channel_name_editor: View<Editor>,
     channel_editing_state: Option<ChannelEditingState>,
     entries: Vec<ListEntry>,
     selection: Option<usize>,
-    user_store: ModelHandle<UserStore>,
+    channel_store: Model<ChannelStore>,
+    user_store: Model<UserStore>,
     client: Arc<Client>,
-    channel_store: ModelHandle<ChannelStore>,
-    project: ModelHandle<Project>,
+    project: Model<Project>,
     match_candidates: Vec<StringMatchCandidate>,
-    list_state: ListState<Self>,
     subscriptions: Vec<Subscription>,
     collapsed_sections: Vec<Section>,
     collapsed_channels: Vec<ChannelId>,
-    drag_target_channel: ChannelDragTarget,
-    workspace: WeakViewHandle<Workspace>,
-    context_menu_on_selected: bool,
-}
-
-#[derive(PartialEq, Eq)]
-enum ChannelDragTarget {
-    None,
-    Root,
-    Channel(ChannelId),
+    workspace: WeakView<Workspace>,
 }
 
 #[derive(Serialize, Deserialize)]
 struct SerializedCollabPanel {
-    width: Option<f32>,
-    collapsed_channels: Option<Vec<ChannelId>>,
-}
-
-#[derive(Debug)]
-pub enum Event {
-    DockPositionChanged,
-    Focus,
-    Dismissed,
+    width: Option<Pixels>,
+    collapsed_channels: Option<Vec<u64>>,
 }
 
 #[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
@@ -365,28 +175,17 @@ enum ListEntry {
     ContactPlaceholder,
 }
 
-impl Entity for CollabPanel {
-    type Event = Event;
-}
-
 impl CollabPanel {
-    pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
-        cx.add_view::<Self, _>(|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);
+    pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
+        cx.new_view(|cx| {
+            let filter_editor = cx.new_view(|cx| {
+                let mut editor = Editor::single_line(cx);
+                editor.set_placeholder_text("Filter...", cx);
                 editor
             });
 
-            cx.subscribe(&filter_editor, |this, _, event, cx| {
-                if let editor::Event::BufferEdited = event {
+            cx.subscribe(&filter_editor, |this: &mut Self, _, event, cx| {
+                if let editor::EditorEvent::BufferEdited = event {
                     let query = this.filter_editor.read(cx).text(cx);
                     if !query.is_empty() {
                         this.selection.take();
@@ -398,27 +197,14 @@ impl CollabPanel {
                             .iter()
                             .position(|entry| !matches!(entry, ListEntry::Header(_)));
                     }
-                } else if let editor::Event::Blurred = event {
-                    let query = this.filter_editor.read(cx).text(cx);
-                    if query.is_empty() {
-                        this.selection.take();
-                        this.update_entries(true, cx);
-                    }
                 }
             })
             .detach();
 
-            let channel_name_editor = cx.add_view(|cx| {
-                Editor::single_line(
-                    Some(Arc::new(|theme| {
-                        theme.collab_panel.user_query_editor.clone()
-                    })),
-                    cx,
-                )
-            });
+            let channel_name_editor = cx.new_view(|cx| Editor::single_line(cx));
 
-            cx.subscribe(&channel_name_editor, |this, _, event, cx| {
-                if let editor::Event::Blurred = event {
+            cx.subscribe(&channel_name_editor, |this: &mut Self, _, event, cx| {
+                if let editor::EditorEvent::Blurred = event {
                     if let Some(state) = &this.channel_editing_state {
                         if state.pending_name().is_some() {
                             return;
@@ -431,151 +217,31 @@ impl CollabPanel {
             })
             .detach();
 
+            let view = cx.view().downgrade();
             let list_state =
-                ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| {
-                    let theme = theme::current(cx).clone();
-                    let is_selected = this.selection == Some(ix);
-                    let current_project_id = this.project.read(cx).remote_id();
-
-                    match &this.entries[ix] {
-                        ListEntry::Header(section) => {
-                            let is_collapsed = this.collapsed_sections.contains(section);
-                            this.render_header(*section, &theme, is_selected, is_collapsed, cx)
-                        }
-                        ListEntry::CallParticipant {
-                            user,
-                            peer_id,
-                            is_pending,
-                        } => Self::render_call_participant(
-                            user,
-                            *peer_id,
-                            this.user_store.clone(),
-                            *is_pending,
-                            is_selected,
-                            &theme,
-                            cx,
-                        ),
-                        ListEntry::ParticipantProject {
-                            project_id,
-                            worktree_root_names,
-                            host_user_id,
-                            is_last,
-                        } => Self::render_participant_project(
-                            *project_id,
-                            worktree_root_names,
-                            *host_user_id,
-                            Some(*project_id) == current_project_id,
-                            *is_last,
-                            is_selected,
-                            &theme,
-                            cx,
-                        ),
-                        ListEntry::ParticipantScreen { peer_id, is_last } => {
-                            Self::render_participant_screen(
-                                *peer_id,
-                                *is_last,
-                                is_selected,
-                                &theme.collab_panel,
-                                cx,
-                            )
-                        }
-                        ListEntry::Channel {
-                            channel,
-                            depth,
-                            has_children,
-                        } => {
-                            let channel_row = this.render_channel(
-                                &*channel,
-                                *depth,
-                                &theme,
-                                is_selected,
-                                *has_children,
-                                ix,
-                                cx,
-                            );
-
-                            if is_selected && this.context_menu_on_selected {
-                                Stack::new()
-                                    .with_child(channel_row)
-                                    .with_child(
-                                        ChildView::new(&this.context_menu, cx)
-                                            .aligned()
-                                            .bottom()
-                                            .right(),
-                                    )
-                                    .into_any()
-                            } else {
-                                return channel_row;
-                            }
-                        }
-                        ListEntry::ChannelNotes { channel_id } => this.render_channel_notes(
-                            *channel_id,
-                            &theme.collab_panel,
-                            is_selected,
-                            ix,
-                            cx,
-                        ),
-                        ListEntry::ChannelChat { channel_id } => this.render_channel_chat(
-                            *channel_id,
-                            &theme.collab_panel,
-                            is_selected,
-                            ix,
-                            cx,
-                        ),
-                        ListEntry::ChannelInvite(channel) => Self::render_channel_invite(
-                            channel.clone(),
-                            this.channel_store.clone(),
-                            &theme.collab_panel,
-                            is_selected,
-                            cx,
-                        ),
-                        ListEntry::IncomingRequest(user) => Self::render_contact_request(
-                            user.clone(),
-                            this.user_store.clone(),
-                            &theme.collab_panel,
-                            true,
-                            is_selected,
-                            cx,
-                        ),
-                        ListEntry::OutgoingRequest(user) => Self::render_contact_request(
-                            user.clone(),
-                            this.user_store.clone(),
-                            &theme.collab_panel,
-                            false,
-                            is_selected,
-                            cx,
-                        ),
-                        ListEntry::Contact { contact, calling } => Self::render_contact(
-                            contact,
-                            *calling,
-                            &this.project,
-                            &theme,
-                            is_selected,
-                            cx,
-                        ),
-                        ListEntry::ChannelEditor { depth } => {
-                            this.render_channel_editor(&theme, *depth, cx)
-                        }
-                        ListEntry::ContactPlaceholder => {
-                            this.render_contact_placeholder(&theme.collab_panel, is_selected, cx)
-                        }
+                ListState::new(0, gpui::ListAlignment::Top, px(1000.), move |ix, cx| {
+                    if let Some(view) = view.upgrade() {
+                        view.update(cx, |view, cx| view.render_list_entry(ix, cx))
+                    } else {
+                        div().into_any()
                     }
                 });
 
             let mut this = Self {
                 width: None,
-                has_focus: false,
+                focus_handle: cx.focus_handle(),
                 channel_clipboard: None,
                 fs: workspace.app_state().fs.clone(),
                 pending_serialization: Task::ready(None),
-                context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
+                context_menu: None,
+                list_state,
                 channel_name_editor,
                 filter_editor,
                 entries: Vec::default(),
                 channel_editing_state: None,
                 selection: None,
-                user_store: workspace.user_store().clone(),
                 channel_store: ChannelStore::global(cx),
+                user_store: workspace.user_store().clone(),
                 project: workspace.project().clone(),
                 subscriptions: Vec::default(),
                 match_candidates: Vec::default(),
@@ -583,26 +249,22 @@ impl CollabPanel {
                 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);
 
             // Update the dock position when the setting changes.
             let mut old_dock_position = this.position(cx);
-            this.subscriptions
-                .push(
-                    cx.observe_global::<SettingsStore, _>(move |this: &mut Self, cx| {
-                        let new_dock_position = this.position(cx);
-                        if new_dock_position != old_dock_position {
-                            old_dock_position = new_dock_position;
-                            cx.emit(Event::DockPositionChanged);
-                        }
-                        cx.notify();
-                    }),
-                );
+            this.subscriptions.push(cx.observe_global::<SettingsStore>(
+                move |this: &mut Self, cx| {
+                    let new_dock_position = this.position(cx);
+                    if new_dock_position != old_dock_position {
+                        old_dock_position = new_dock_position;
+                        cx.emit(PanelEvent::ChangePosition);
+                    }
+                    cx.notify();
+                },
+            ));
 
             let active_call = ActiveCall::global(cx);
             this.subscriptions
@@ -642,49 +304,41 @@ impl CollabPanel {
         })
     }
 
-    pub fn load(
-        workspace: WeakViewHandle<Workspace>,
-        cx: AsyncAppContext,
-    ) -> Task<Result<ViewHandle<Self>>> {
-        cx.spawn(|mut cx| async move {
-            let serialized_panel = if let Some(panel) = cx
-                .background()
-                .spawn(async move { KEY_VALUE_STORE.read_kvp(COLLABORATION_PANEL_KEY) })
-                .await
-                .log_err()
-                .flatten()
-            {
-                match serde_json::from_str::<SerializedCollabPanel>(&panel) {
-                    Ok(panel) => Some(panel),
-                    Err(err) => {
-                        log::error!("Failed to deserialize collaboration panel: {}", err);
-                        None
-                    }
-                }
-            } else {
-                None
-            };
-
-            workspace.update(&mut cx, |workspace, cx| {
-                let panel = CollabPanel::new(workspace, cx);
-                if let Some(serialized_panel) = serialized_panel {
-                    panel.update(cx, |panel, cx| {
-                        panel.width = serialized_panel.width;
-                        panel.collapsed_channels = serialized_panel
-                            .collapsed_channels
-                            .unwrap_or_else(|| Vec::new());
-                        cx.notify();
-                    });
-                }
-                panel
-            })
+    pub async fn load(
+        workspace: WeakView<Workspace>,
+        mut cx: AsyncWindowContext,
+    ) -> anyhow::Result<View<Self>> {
+        let serialized_panel = cx
+            .background_executor()
+            .spawn(async move { KEY_VALUE_STORE.read_kvp(COLLABORATION_PANEL_KEY) })
+            .await
+            .map_err(|_| anyhow::anyhow!("Failed to read collaboration panel from key value store"))
+            .log_err()
+            .flatten()
+            .map(|panel| serde_json::from_str::<SerializedCollabPanel>(&panel))
+            .transpose()
+            .log_err()
+            .flatten();
+
+        workspace.update(&mut cx, |workspace, cx| {
+            let panel = CollabPanel::new(workspace, cx);
+            if let Some(serialized_panel) = serialized_panel {
+                panel.update(cx, |panel, cx| {
+                    panel.width = serialized_panel.width;
+                    panel.collapsed_channels = serialized_panel
+                        .collapsed_channels
+                        .unwrap_or_else(|| Vec::new());
+                    cx.notify();
+                });
+            }
+            panel
         })
     }
 
     fn serialize(&mut self, cx: &mut ViewContext<Self>) {
         let width = self.width;
         let collapsed_channels = self.collapsed_channels.clone();
-        self.pending_serialization = cx.background().spawn(
+        self.pending_serialization = cx.background_executor().spawn(
             async move {
                 KEY_VALUE_STORE
                     .write_kvp(
@@ -701,11 +355,15 @@ impl CollabPanel {
         );
     }
 
+    fn scroll_to_item(&mut self, ix: usize) {
+        self.list_state.scroll_to_reveal_item(ix)
+    }
+
     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 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);
@@ -851,7 +509,7 @@ impl CollabPanel {
                     .extend(channel_store.ordered_channels().enumerate().map(
                         |(ix, (_, channel))| StringMatchCandidate {
                             id: ix,
-                            string: channel.name.clone(),
+                            string: channel.name.clone().into(),
                             char_bag: channel.name.chars().collect(),
                         },
                     ));
@@ -929,7 +587,7 @@ impl CollabPanel {
                     .extend(channel_invites.iter().enumerate().map(|(ix, channel)| {
                         StringMatchCandidate {
                             id: ix,
-                            string: channel.name.clone(),
+                            string: channel.name.clone().into(),
                             char_bag: channel.name.chars().collect(),
                         }
                     }));
@@ -1097,7 +755,6 @@ impl CollabPanel {
         }
 
         let old_scroll_top = self.list_state.logical_scroll_top();
-
         self.list_state.reset(self.entries.len());
 
         if scroll_to_top {
@@ -1121,7 +778,7 @@ impl CollabPanel {
                             .position(|entry| entry == entry_after_old_top)?;
                         Some(ListOffset {
                             item_ix,
-                            offset_in_item: 0.,
+                            offset_in_item: Pixels::ZERO,
                         })
                     })
                     .or_else(|| {
@@ -1133,7 +790,7 @@ impl CollabPanel {
                             .position(|entry| entry == entry_before_old_top)?;
                         Some(ListOffset {
                             item_ix,
-                            offset_in_item: 0.,
+                            offset_in_item: Pixels::ZERO,
                         })
                     });
 
@@ -1146,288 +803,107 @@ impl CollabPanel {
     }
 
     fn render_call_participant(
-        user: &User,
+        &self,
+        user: &Arc<User>,
         peer_id: Option<PeerId>,
-        user_store: ModelHandle<UserStore>,
         is_pending: bool,
         is_selected: bool,
-        theme: &theme::Theme,
         cx: &mut ViewContext<Self>,
-    ) -> AnyElement<Self> {
-        enum CallParticipant {}
-        enum CallParticipantTooltip {}
-        enum LeaveCallButton {}
-        enum LeaveCallTooltip {}
-
-        let collab_theme = &theme.collab_panel;
-
+    ) -> ListItem {
         let is_current_user =
-            user_store.read(cx).current_user().map(|user| user.id) == Some(user.id);
-
-        let content = MouseEventHandler::new::<CallParticipant, _>(
-            user.id as usize,
-            cx,
-            |mouse_state, cx| {
-                let style = if is_current_user {
-                    *collab_theme
-                        .contact_row
-                        .in_state(is_selected)
-                        .style_for(&mut Default::default())
-                } else {
-                    *collab_theme
-                        .contact_row
-                        .in_state(is_selected)
-                        .style_for(mouse_state)
-                };
-
-                Flex::row()
-                    .with_children(user.avatar.clone().map(|avatar| {
-                        Image::from_data(avatar)
-                            .with_style(collab_theme.contact_avatar)
-                            .aligned()
-                            .left()
-                    }))
-                    .with_child(
-                        Label::new(
-                            user.github_login.clone(),
-                            collab_theme.contact_username.text.clone(),
-                        )
-                        .contained()
-                        .with_style(collab_theme.contact_username.container)
-                        .aligned()
-                        .left()
-                        .flex(1., true),
-                    )
-                    .with_children(if is_pending {
-                        Some(
-                            Label::new("Calling", collab_theme.calling_indicator.text.clone())
-                                .contained()
-                                .with_style(collab_theme.calling_indicator.container)
-                                .aligned()
-                                .into_any(),
-                        )
-                    } else if is_current_user {
-                        Some(
-                            MouseEventHandler::new::<LeaveCallButton, _>(0, cx, |state, _| {
-                                render_icon_button(
-                                    theme
-                                        .collab_panel
-                                        .leave_call_button
-                                        .style_for(is_selected, state),
-                                    "icons/exit.svg",
-                                )
-                            })
-                            .with_cursor_style(CursorStyle::PointingHand)
-                            .on_click(MouseButton::Left, |_, _, cx| {
-                                Self::leave_call(cx);
-                            })
-                            .with_tooltip::<LeaveCallTooltip>(
-                                0,
-                                "Leave call",
-                                None,
-                                theme.tooltip.clone(),
-                                cx,
-                            )
-                            .into_any(),
-                        )
-                    } else {
-                        None
-                    })
-                    .constrained()
-                    .with_height(collab_theme.row_height)
-                    .contained()
-                    .with_style(style)
-            },
-        );
-
-        if is_current_user || is_pending || peer_id.is_none() {
-            return content.into_any();
-        }
-
+            self.user_store.read(cx).current_user().map(|user| user.id) == Some(user.id);
         let tooltip = format!("Follow {}", user.github_login);
 
-        content
-            .on_click(MouseButton::Left, move |_, this, cx| {
-                if let Some(workspace) = this.workspace.upgrade(cx) {
-                    workspace
-                        .update(cx, |workspace, cx| workspace.follow(peer_id.unwrap(), cx))
-                        .map(|task| task.detach_and_log_err(cx));
-                }
+        ListItem::new(SharedString::from(user.github_login.clone()))
+            .start_slot(Avatar::new(user.avatar_uri.clone()))
+            .child(Label::new(user.github_login.clone()))
+            .selected(is_selected)
+            .end_slot(if is_pending {
+                Label::new("Calling").color(Color::Muted).into_any_element()
+            } else if is_current_user {
+                IconButton::new("leave-call", Icon::Exit)
+                    .style(ButtonStyle::Subtle)
+                    .on_click(move |_, cx| Self::leave_call(cx))
+                    .tooltip(|cx| Tooltip::text("Leave Call", cx))
+                    .into_any_element()
+            } else {
+                div().into_any_element()
+            })
+            .when_some(peer_id, |this, peer_id| {
+                this.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
+                    .on_click(cx.listener(move |this, _, cx| {
+                        this.workspace
+                            .update(cx, |workspace, cx| workspace.follow(peer_id, cx))
+                            .ok();
+                    }))
             })
-            .with_cursor_style(CursorStyle::PointingHand)
-            .with_tooltip::<CallParticipantTooltip>(
-                user.id as usize,
-                tooltip,
-                Some(Box::new(FollowNextCollaborator)),
-                theme.tooltip.clone(),
-                cx,
-            )
-            .into_any()
     }
 
     fn render_participant_project(
+        &self,
         project_id: u64,
         worktree_root_names: &[String],
         host_user_id: u64,
-        is_current: bool,
         is_last: bool,
         is_selected: bool,
-        theme: &theme::Theme,
         cx: &mut ViewContext<Self>,
-    ) -> AnyElement<Self> {
-        enum JoinProject {}
-        enum JoinProjectTooltip {}
-
-        let collab_theme = &theme.collab_panel;
-        let host_avatar_width = collab_theme
-            .contact_avatar
-            .width
-            .or(collab_theme.contact_avatar.height)
-            .unwrap_or(0.);
-        let tree_branch = collab_theme.tree_branch;
-        let project_name = if worktree_root_names.is_empty() {
+    ) -> impl IntoElement {
+        let project_name: SharedString = if worktree_root_names.is_empty() {
             "untitled".to_string()
         } else {
             worktree_root_names.join(", ")
-        };
-
-        let content =
-            MouseEventHandler::new::<JoinProject, _>(project_id as usize, cx, |mouse_state, cx| {
-                let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
-                let row = if is_current {
-                    collab_theme
-                        .project_row
-                        .in_state(true)
-                        .style_for(&mut Default::default())
-                } else {
-                    collab_theme
-                        .project_row
-                        .in_state(is_selected)
-                        .style_for(mouse_state)
-                };
-
-                Flex::row()
-                    .with_child(render_tree_branch(
-                        tree_branch,
-                        &row.name.text,
-                        is_last,
-                        vec2f(host_avatar_width, collab_theme.row_height),
-                        cx.font_cache(),
-                    ))
-                    .with_child(
-                        Svg::new("icons/file_icons/folder.svg")
-                            .with_color(collab_theme.channel_hash.color)
-                            .constrained()
-                            .with_width(collab_theme.channel_hash.width)
-                            .aligned()
-                            .left(),
-                    )
-                    .with_child(
-                        Label::new(project_name.clone(), row.name.text.clone())
-                            .aligned()
-                            .left()
-                            .contained()
-                            .with_style(row.name.container)
-                            .flex(1., false),
-                    )
-                    .constrained()
-                    .with_height(collab_theme.row_height)
-                    .contained()
-                    .with_style(row.container)
-            });
-
-        if is_current {
-            return content.into_any();
         }
-
-        content
-            .with_cursor_style(CursorStyle::PointingHand)
-            .on_click(MouseButton::Left, move |_, this, cx| {
-                if let Some(workspace) = this.workspace.upgrade(cx) {
-                    let app_state = workspace.read(cx).app_state().clone();
-                    workspace::join_remote_project(project_id, host_user_id, app_state, cx)
-                        .detach_and_log_err(cx);
-                }
-            })
-            .with_tooltip::<JoinProjectTooltip>(
-                project_id as usize,
-                format!("Open {}", project_name),
-                None,
-                theme.tooltip.clone(),
-                cx,
+        .into();
+
+        ListItem::new(project_id as usize)
+            .selected(is_selected)
+            .on_click(cx.listener(move |this, _, cx| {
+                this.workspace
+                    .update(cx, |workspace, cx| {
+                        let app_state = workspace.app_state().clone();
+                        workspace::join_remote_project(project_id, host_user_id, app_state, cx)
+                            .detach_and_log_err(cx);
+                    })
+                    .ok();
+            }))
+            .start_slot(
+                h_stack()
+                    .gap_1()
+                    .child(render_tree_branch(is_last, cx))
+                    .child(IconButton::new(0, Icon::Folder)),
             )
-            .into_any()
+            .child(Label::new(project_name.clone()))
+            .tooltip(move |cx| Tooltip::text(format!("Open {}", project_name), cx))
     }
 
     fn render_participant_screen(
+        &self,
         peer_id: Option<PeerId>,
         is_last: bool,
         is_selected: bool,
-        theme: &theme::CollabPanel,
         cx: &mut ViewContext<Self>,
-    ) -> AnyElement<Self> {
-        enum OpenSharedScreen {}
-
-        let host_avatar_width = theme
-            .contact_avatar
-            .width
-            .or(theme.contact_avatar.height)
-            .unwrap_or(0.);
-        let tree_branch = theme.tree_branch;
-
-        let handler = MouseEventHandler::new::<OpenSharedScreen, _>(
-            peer_id.map(|id| id.as_u64()).unwrap_or(0) as usize,
-            cx,
-            |mouse_state, cx| {
-                let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
-                let row = theme
-                    .project_row
-                    .in_state(is_selected)
-                    .style_for(mouse_state);
-
-                Flex::row()
-                    .with_child(render_tree_branch(
-                        tree_branch,
-                        &row.name.text,
-                        is_last,
-                        vec2f(host_avatar_width, theme.row_height),
-                        cx.font_cache(),
-                    ))
-                    .with_child(
-                        Svg::new("icons/desktop.svg")
-                            .with_color(theme.channel_hash.color)
-                            .constrained()
-                            .with_width(theme.channel_hash.width)
-                            .aligned()
-                            .left(),
-                    )
-                    .with_child(
-                        Label::new("Screen", row.name.text.clone())
-                            .aligned()
-                            .left()
-                            .contained()
-                            .with_style(row.name.container)
-                            .flex(1., false),
-                    )
-                    .constrained()
-                    .with_height(theme.row_height)
-                    .contained()
-                    .with_style(row.container)
-            },
-        );
-        if peer_id.is_none() {
-            return handler.into_any();
-        }
-        handler
-            .with_cursor_style(CursorStyle::PointingHand)
-            .on_click(MouseButton::Left, move |_, this, cx| {
-                if let Some(workspace) = this.workspace.upgrade(cx) {
-                    workspace.update(cx, |workspace, cx| {
-                        workspace.open_shared_screen(peer_id.unwrap(), cx)
-                    });
-                }
+    ) -> impl IntoElement {
+        let id = peer_id.map_or(usize::MAX, |id| id.as_u64() as usize);
+
+        ListItem::new(("screen", id))
+            .selected(is_selected)
+            .start_slot(
+                h_stack()
+                    .gap_1()
+                    .child(render_tree_branch(is_last, cx))
+                    .child(IconButton::new(0, Icon::Screen)),
+            )
+            .child(Label::new("Screen"))
+            .when_some(peer_id, |this, _| {
+                this.on_click(cx.listener(move |this, _, cx| {
+                    this.workspace
+                        .update(cx, |workspace, cx| {
+                            workspace.open_shared_screen(peer_id.unwrap(), cx)
+                        })
+                        .ok();
+                }))
+                .tooltip(move |cx| Tooltip::text(format!("Open shared screen"), cx))
             })
-            .into_any()
     }
 
     fn take_editing_state(&mut self, cx: &mut ViewContext<Self>) -> bool {

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

@@ -3,19 +3,17 @@ use client::{
     proto::{self, ChannelRole, ChannelVisibility},
     User, UserId, UserStore,
 };
-use context_menu::{ContextMenu, ContextMenuItem};
 use fuzzy::{match_strings, StringMatchCandidate};
 use gpui::{
-    actions,
-    elements::*,
-    platform::{CursorStyle, MouseButton},
-    AppContext, ClipboardItem, Entity, ModelHandle, MouseState, Task, View, ViewContext,
-    ViewHandle,
+    actions, div, overlay, AppContext, ClipboardItem, DismissEvent, EventEmitter, FocusableView,
+    Model, ParentElement, Render, Styled, Subscription, Task, View, ViewContext, VisualContext,
+    WeakView,
 };
-use picker::{Picker, PickerDelegate, PickerEvent};
+use picker::{Picker, PickerDelegate};
 use std::sync::Arc;
+use ui::{prelude::*, Avatar, Checkbox, ContextMenu, ListItem, ListItemSpacing};
 use util::TryFutureExt;
-use workspace::Modal;
+use workspace::ModalView;
 
 actions!(
     channel_modal,
@@ -27,34 +25,27 @@ actions!(
     ]
 );
 
-pub fn init(cx: &mut AppContext) {
-    Picker::<ChannelModalDelegate>::init(cx);
-    cx.add_action(ChannelModal::toggle_mode);
-    cx.add_action(ChannelModal::toggle_member_admin);
-    cx.add_action(ChannelModal::remove_member);
-    cx.add_action(ChannelModal::dismiss);
-}
-
 pub struct ChannelModal {
-    picker: ViewHandle<Picker<ChannelModalDelegate>>,
-    channel_store: ModelHandle<ChannelStore>,
+    picker: View<Picker<ChannelModalDelegate>>,
+    channel_store: Model<ChannelStore>,
     channel_id: ChannelId,
-    has_focus: bool,
 }
 
 impl ChannelModal {
     pub fn new(
-        user_store: ModelHandle<UserStore>,
-        channel_store: ModelHandle<ChannelStore>,
+        user_store: Model<UserStore>,
+        channel_store: Model<ChannelStore>,
         channel_id: ChannelId,
         mode: Mode,
         members: Vec<ChannelMembership>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
         cx.observe(&channel_store, |_, _, cx| cx.notify()).detach();
-        let picker = cx.add_view(|cx| {
+        let channel_modal = cx.view().downgrade();
+        let picker = cx.new_view(|cx| {
             Picker::new(
                 ChannelModalDelegate {
+                    channel_modal,
                     matching_users: Vec::new(),
                     matching_member_indices: Vec::new(),
                     selected_index: 0,
@@ -62,33 +53,24 @@ impl ChannelModal {
                     channel_store: channel_store.clone(),
                     channel_id,
                     match_candidates: Vec::new(),
+                    context_menu: None,
                     members,
                     mode,
-                    context_menu: cx.add_view(|cx| {
-                        let mut menu = ContextMenu::new(cx.view_id(), cx);
-                        menu.set_position_mode(OverlayPositionMode::Local);
-                        menu
-                    }),
                 },
                 cx,
             )
-            .with_theme(|theme| theme.collab_panel.tabbed_modal.picker.clone())
+            .modal(false)
         });
 
-        cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach();
-
-        let has_focus = picker.read(cx).has_focus();
-
         Self {
             picker,
             channel_store,
             channel_id,
-            has_focus,
         }
     }
 
     fn toggle_mode(&mut self, _: &ToggleMode, cx: &mut ViewContext<Self>) {
-        let mode = match self.picker.read(cx).delegate().mode {
+        let mode = match self.picker.read(cx).delegate.mode {
             Mode::ManageMembers => Mode::InviteMembers,
             Mode::InviteMembers => Mode::ManageMembers,
         };
@@ -103,20 +85,20 @@ impl ChannelModal {
                 let mut members = channel_store
                     .update(&mut cx, |channel_store, cx| {
                         channel_store.get_channel_member_details(channel_id, cx)
-                    })
+                    })?
                     .await?;
 
                 members.sort_by(|a, b| a.sort_key().cmp(&b.sort_key()));
 
                 this.update(&mut cx, |this, cx| {
                     this.picker
-                        .update(cx, |picker, _| picker.delegate_mut().members = members);
+                        .update(cx, |picker, _| picker.delegate.members = members);
                 })?;
             }
 
             this.update(&mut cx, |this, cx| {
                 this.picker.update(cx, |picker, cx| {
-                    let delegate = picker.delegate_mut();
+                    let delegate = &mut picker.delegate;
                     delegate.mode = mode;
                     delegate.selected_index = 0;
                     picker.set_query("", cx);
@@ -129,204 +111,118 @@ impl ChannelModal {
         .detach();
     }
 
-    fn toggle_member_admin(&mut self, _: &ToggleMemberAdmin, cx: &mut ViewContext<Self>) {
-        self.picker.update(cx, |picker, cx| {
-            picker.delegate_mut().toggle_selected_member_admin(cx);
-        })
-    }
-
-    fn remove_member(&mut self, _: &RemoveMember, cx: &mut ViewContext<Self>) {
-        self.picker.update(cx, |picker, cx| {
-            picker.delegate_mut().remove_selected_member(cx);
+    fn set_channel_visiblity(&mut self, selection: &Selection, cx: &mut ViewContext<Self>) {
+        self.channel_store.update(cx, |channel_store, cx| {
+            channel_store
+                .set_channel_visibility(
+                    self.channel_id,
+                    match selection {
+                        Selection::Unselected => ChannelVisibility::Members,
+                        Selection::Selected => ChannelVisibility::Public,
+                        Selection::Indeterminate => return,
+                    },
+                    cx,
+                )
+                .detach_and_log_err(cx)
         });
     }
 
     fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
-        cx.emit(PickerEvent::Dismiss);
+        cx.emit(DismissEvent);
     }
 }
 
-impl Entity for ChannelModal {
-    type Event = PickerEvent;
-}
+impl EventEmitter<DismissEvent> for ChannelModal {}
+impl ModalView for ChannelModal {}
 
-impl View for ChannelModal {
-    fn ui_name() -> &'static str {
-        "ChannelModal"
+impl FocusableView for ChannelModal {
+    fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
+        self.picker.focus_handle(cx)
     }
+}
 
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let theme = &theme::current(cx).collab_panel.tabbed_modal;
-
-        let mode = self.picker.read(cx).delegate().mode;
-        let Some(channel) = self.channel_store.read(cx).channel_for_id(self.channel_id) else {
-            return Empty::new().into_any();
+impl Render for ChannelModal {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+        let channel_store = self.channel_store.read(cx);
+        let Some(channel) = channel_store.channel_for_id(self.channel_id) else {
+            return div();
         };
-
-        enum InviteMembers {}
-        enum ManageMembers {}
-
-        fn render_mode_button<T: 'static>(
-            mode: Mode,
-            text: &'static str,
-            current_mode: Mode,
-            theme: &theme::TabbedModal,
-            cx: &mut ViewContext<ChannelModal>,
-        ) -> AnyElement<ChannelModal> {
-            let active = mode == current_mode;
-            MouseEventHandler::new::<T, _>(0, cx, move |state, _| {
-                let contained_text = theme.tab_button.style_for(active, state);
-                Label::new(text, contained_text.text.clone())
-                    .contained()
-                    .with_style(contained_text.container.clone())
-            })
-            .on_click(MouseButton::Left, move |_, this, cx| {
-                if !active {
-                    this.set_mode(mode, cx);
-                }
-            })
-            .with_cursor_style(CursorStyle::PointingHand)
-            .into_any()
-        }
-
-        fn render_visibility(
-            channel_id: ChannelId,
-            visibility: ChannelVisibility,
-            theme: &theme::TabbedModal,
-            cx: &mut ViewContext<ChannelModal>,
-        ) -> AnyElement<ChannelModal> {
-            enum TogglePublic {}
-
-            if visibility == ChannelVisibility::Members {
-                return Flex::row()
-                    .with_child(
-                        MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
-                            let style = theme.visibility_toggle.style_for(state);
-                            Label::new(format!("{}", "Public access: OFF"), style.text.clone())
-                                .contained()
-                                .with_style(style.container.clone())
-                        })
-                        .on_click(MouseButton::Left, move |_, this, cx| {
-                            this.channel_store
-                                .update(cx, |channel_store, cx| {
-                                    channel_store.set_channel_visibility(
-                                        channel_id,
-                                        ChannelVisibility::Public,
-                                        cx,
+        let channel_name = channel.name.clone();
+        let channel_id = channel.id;
+        let visibility = channel.visibility;
+        let mode = self.picker.read(cx).delegate.mode;
+
+        v_stack()
+            .key_context("ChannelModal")
+            .on_action(cx.listener(Self::toggle_mode))
+            .on_action(cx.listener(Self::dismiss))
+            .elevation_3(cx)
+            .w(rems(34.))
+            .child(
+                v_stack()
+                    .px_2()
+                    .py_1()
+                    .rounded_t(px(8.))
+                    .bg(cx.theme().colors().element_background)
+                    .child(IconElement::new(Icon::Hash).size(IconSize::Medium))
+                    .child(Label::new(channel_name))
+                    .child(
+                        h_stack()
+                            .w_full()
+                            .justify_between()
+                            .child(
+                                h_stack()
+                                    .gap_2()
+                                    .child(
+                                        Checkbox::new(
+                                            "is-public",
+                                            if visibility == ChannelVisibility::Public {
+                                                ui::Selection::Selected
+                                            } else {
+                                                ui::Selection::Unselected
+                                            },
+                                        )
+                                        .on_click(cx.listener(Self::set_channel_visiblity)),
                                     )
-                                })
-                                .detach_and_log_err(cx);
-                        })
-                        .with_cursor_style(CursorStyle::PointingHand),
-                    )
-                    .into_any();
-            }
-
-            Flex::row()
-                .with_child(
-                    MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
-                        let style = theme.visibility_toggle.style_for(state);
-                        Label::new(format!("{}", "Public access: ON"), style.text.clone())
-                            .contained()
-                            .with_style(style.container.clone())
-                    })
-                    .on_click(MouseButton::Left, move |_, this, cx| {
-                        this.channel_store
-                            .update(cx, |channel_store, cx| {
-                                channel_store.set_channel_visibility(
-                                    channel_id,
-                                    ChannelVisibility::Members,
-                                    cx,
-                                )
-                            })
-                            .detach_and_log_err(cx);
-                    })
-                    .with_cursor_style(CursorStyle::PointingHand),
-                )
-                .with_spacing(14.0)
-                .with_child(
-                    MouseEventHandler::new::<TogglePublic, _>(1, cx, move |state, _| {
-                        let style = theme.channel_link.style_for(state);
-                        Label::new(format!("{}", "copy link"), style.text.clone())
-                            .contained()
-                            .with_style(style.container.clone())
-                    })
-                    .on_click(MouseButton::Left, move |_, this, cx| {
-                        if let Some(channel) =
-                            this.channel_store.read(cx).channel_for_id(channel_id)
-                        {
-                            let item = ClipboardItem::new(channel.link());
-                            cx.write_to_clipboard(item);
-                        }
-                    })
-                    .with_cursor_style(CursorStyle::PointingHand),
-                )
-                .into_any()
-        }
-
-        Flex::column()
-            .with_child(
-                Flex::column()
-                    .with_child(
-                        Label::new(format!("#{}", channel.name), theme.title.text.clone())
-                            .contained()
-                            .with_style(theme.title.container.clone()),
+                                    .child(Label::new("Public")),
+                            )
+                            .children(if visibility == ChannelVisibility::Public {
+                                Some(Button::new("copy-link", "Copy Link").on_click(cx.listener(
+                                    move |this, _, cx| {
+                                        if let Some(channel) =
+                                            this.channel_store.read(cx).channel_for_id(channel_id)
+                                        {
+                                            let item = ClipboardItem::new(channel.link());
+                                            cx.write_to_clipboard(item);
+                                        }
+                                    },
+                                )))
+                            } else {
+                                None
+                            }),
                     )
-                    .with_child(render_visibility(channel.id, channel.visibility, theme, cx))
-                    .with_child(Flex::row().with_children([
-                        render_mode_button::<InviteMembers>(
-                            Mode::InviteMembers,
-                            "Invite members",
-                            mode,
-                            theme,
-                            cx,
-                        ),
-                        render_mode_button::<ManageMembers>(
-                            Mode::ManageMembers,
-                            "Manage members",
-                            mode,
-                            theme,
-                            cx,
-                        ),
-                    ]))
-                    .expanded()
-                    .contained()
-                    .with_style(theme.header),
-            )
-            .with_child(
-                ChildView::new(&self.picker, cx)
-                    .contained()
-                    .with_style(theme.body),
+                    .child(
+                        div()
+                            .w_full()
+                            .flex()
+                            .flex_row()
+                            .child(
+                                Button::new("manage-members", "Manage Members")
+                                    .selected(mode == Mode::ManageMembers)
+                                    .on_click(cx.listener(|this, _, cx| {
+                                        this.set_mode(Mode::ManageMembers, cx);
+                                    })),
+                            )
+                            .child(
+                                Button::new("invite-members", "Invite Members")
+                                    .selected(mode == Mode::InviteMembers)
+                                    .on_click(cx.listener(|this, _, cx| {
+                                        this.set_mode(Mode::InviteMembers, cx);
+                                    })),
+                            ),
+                    ),
             )
-            .constrained()
-            .with_max_height(theme.max_height)
-            .with_max_width(theme.max_width)
-            .contained()
-            .with_style(theme.modal)
-            .into_any()
-    }
-
-    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;
-    }
-}
-
-impl Modal for ChannelModal {
-    fn has_focus(&self) -> bool {
-        self.has_focus
-    }
-
-    fn dismiss_on_event(event: &Self::Event) -> bool {
-        match event {
-            PickerEvent::Dismiss => true,
-        }
+            .child(self.picker.clone())
     }
 }
 
@@ -337,19 +233,22 @@ pub enum Mode {
 }
 
 pub struct ChannelModalDelegate {
+    channel_modal: WeakView<ChannelModal>,
     matching_users: Vec<Arc<User>>,
     matching_member_indices: Vec<usize>,
-    user_store: ModelHandle<UserStore>,
-    channel_store: ModelHandle<ChannelStore>,
+    user_store: Model<UserStore>,
+    channel_store: Model<ChannelStore>,
     channel_id: ChannelId,
     selected_index: usize,
     mode: Mode,
     match_candidates: Vec<StringMatchCandidate>,
     members: Vec<ChannelMembership>,
-    context_menu: ViewHandle<ContextMenu>,
+    context_menu: Option<(View<ContextMenu>, Subscription)>,
 }
 
 impl PickerDelegate for ChannelModalDelegate {
+    type ListItem = ListItem;
+
     fn placeholder_text(&self) -> Arc<str> {
         "Search collaborator by username...".into()
     }
@@ -382,19 +281,19 @@ impl PickerDelegate for ChannelModalDelegate {
                         }
                     }));
 
-                let matches = cx.background().block(match_strings(
+                let matches = cx.background_executor().block(match_strings(
                     &self.match_candidates,
                     &query,
                     true,
                     usize::MAX,
                     &Default::default(),
-                    cx.background().clone(),
+                    cx.background_executor().clone(),
                 ));
 
                 cx.spawn(|picker, mut cx| async move {
                     picker
                         .update(&mut cx, |picker, cx| {
-                            let delegate = picker.delegate_mut();
+                            let delegate = &mut picker.delegate;
                             delegate.matching_member_indices.clear();
                             delegate
                                 .matching_member_indices
@@ -412,8 +311,7 @@ impl PickerDelegate for ChannelModalDelegate {
                     async {
                         let users = search_users.await?;
                         picker.update(&mut cx, |picker, cx| {
-                            let delegate = picker.delegate_mut();
-                            delegate.matching_users = users;
+                            picker.delegate.matching_users = users;
                             cx.notify();
                         })?;
                         anyhow::Ok(())
@@ -429,11 +327,11 @@ impl PickerDelegate for ChannelModalDelegate {
         if let Some((selected_user, role)) = self.user_at_index(self.selected_index) {
             match self.mode {
                 Mode::ManageMembers => {
-                    self.show_context_menu(role.unwrap_or(ChannelRole::Member), cx)
+                    self.show_context_menu(selected_user, role.unwrap_or(ChannelRole::Member), cx)
                 }
                 Mode::InviteMembers => match self.member_status(selected_user.id, cx) {
                     Some(proto::channel_member::Kind::Invitee) => {
-                        self.remove_selected_member(cx);
+                        self.remove_member(selected_user.id, cx);
                     }
                     Some(proto::channel_member::Kind::AncestorMember) | None => {
                         self.invite_member(selected_user, cx)
@@ -445,138 +343,70 @@ impl PickerDelegate for ChannelModalDelegate {
     }
 
     fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
-        cx.emit(PickerEvent::Dismiss);
+        if self.context_menu.is_none() {
+            self.channel_modal
+                .update(cx, |_, cx| {
+                    cx.emit(DismissEvent);
+                })
+                .ok();
+        }
     }
 
     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.channel_modal;
-        let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
-        let (user, role) = self.user_at_index(ix).unwrap();
+        cx: &mut ViewContext<Picker<Self>>,
+    ) -> Option<Self::ListItem> {
+        let (user, role) = self.user_at_index(ix)?;
         let request_status = self.member_status(user.id, cx);
 
-        let style = tabbed_modal
-            .picker
-            .item
-            .in_state(selected)
-            .style_for(mouse_state);
-
-        let in_manage = matches!(self.mode, Mode::ManageMembers);
-
-        let mut result = 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({
-                (in_manage && request_status == Some(proto::channel_member::Kind::Invitee)).then(
-                    || {
-                        Label::new("Invited", theme.member_tag.text.clone())
-                            .contained()
-                            .with_style(theme.member_tag.container)
-                            .aligned()
-                            .left()
-                    },
-                )
-            })
-            .with_children(if in_manage && role == Some(ChannelRole::Admin) {
-                Some(
-                    Label::new("Admin", theme.member_tag.text.clone())
-                        .contained()
-                        .with_style(theme.member_tag.container)
-                        .aligned()
-                        .left(),
-                )
-            } else if in_manage && role == Some(ChannelRole::Guest) {
-                Some(
-                    Label::new("Guest", theme.member_tag.text.clone())
-                        .contained()
-                        .with_style(theme.member_tag.container)
-                        .aligned()
-                        .left(),
-                )
-            } else {
-                None
-            })
-            .with_children({
-                let svg = match self.mode {
-                    Mode::ManageMembers => Some(
-                        Svg::new("icons/ellipsis.svg")
-                            .with_color(theme.member_icon.color)
-                            .constrained()
-                            .with_width(theme.member_icon.icon_width)
-                            .aligned()
-                            .constrained()
-                            .with_width(theme.member_icon.button_width)
-                            .with_height(theme.member_icon.button_width)
-                            .contained()
-                            .with_style(theme.member_icon.container),
-                    ),
-                    Mode::InviteMembers => match request_status {
-                        Some(proto::channel_member::Kind::Member) => Some(
-                            Svg::new("icons/check.svg")
-                                .with_color(theme.member_icon.color)
-                                .constrained()
-                                .with_width(theme.member_icon.icon_width)
-                                .aligned()
-                                .constrained()
-                                .with_width(theme.member_icon.button_width)
-                                .with_height(theme.member_icon.button_width)
-                                .contained()
-                                .with_style(theme.member_icon.container),
-                        ),
-                        Some(proto::channel_member::Kind::Invitee) => Some(
-                            Svg::new("icons/check.svg")
-                                .with_color(theme.invitee_icon.color)
-                                .constrained()
-                                .with_width(theme.invitee_icon.icon_width)
-                                .aligned()
-                                .constrained()
-                                .with_width(theme.invitee_icon.button_width)
-                                .with_height(theme.invitee_icon.button_width)
-                                .contained()
-                                .with_style(theme.invitee_icon.container),
-                        ),
-                        Some(proto::channel_member::Kind::AncestorMember) | None => None,
-                    },
-                };
-
-                svg.map(|svg| svg.aligned().flex_float().into_any())
-            })
-            .contained()
-            .with_style(style.container)
-            .constrained()
-            .with_height(tabbed_modal.row_height)
-            .into_any();
-
-        if selected {
-            result = Stack::new()
-                .with_child(result)
-                .with_child(
-                    ChildView::new(&self.context_menu, cx)
-                        .aligned()
-                        .top()
-                        .right(),
-                )
-                .into_any();
-        }
-
-        result
+        Some(
+            ListItem::new(ix)
+                .inset(true)
+                .spacing(ListItemSpacing::Sparse)
+                .selected(selected)
+                .start_slot(Avatar::new(user.avatar_uri.clone()))
+                .child(Label::new(user.github_login.clone()))
+                .end_slot(h_stack().gap_2().map(|slot| {
+                    match self.mode {
+                        Mode::ManageMembers => slot
+                            .children(
+                                if request_status == Some(proto::channel_member::Kind::Invitee) {
+                                    Some(Label::new("Invited"))
+                                } else {
+                                    None
+                                },
+                            )
+                            .children(match role {
+                                Some(ChannelRole::Admin) => Some(Label::new("Admin")),
+                                Some(ChannelRole::Guest) => Some(Label::new("Guest")),
+                                _ => None,
+                            })
+                            .child(IconButton::new("ellipsis", Icon::Ellipsis))
+                            .children(
+                                if let (Some((menu, _)), true) = (&self.context_menu, selected) {
+                                    Some(
+                                        overlay()
+                                            .anchor(gpui::AnchorCorner::TopLeft)
+                                            .child(menu.clone()),
+                                    )
+                                } else {
+                                    None
+                                },
+                            ),
+                        Mode::InviteMembers => match request_status {
+                            Some(proto::channel_member::Kind::Invitee) => {
+                                slot.children(Some(Label::new("Invited")))
+                            }
+                            Some(proto::channel_member::Kind::Member) => {
+                                slot.children(Some(Label::new("Member")))
+                            }
+                            _ => slot,
+                        },
+                    }
+                })),
+        )
     }
 }
 
@@ -610,21 +440,20 @@ impl ChannelModalDelegate {
         }
     }
 
-    fn toggle_selected_member_admin(&mut self, cx: &mut ViewContext<Picker<Self>>) -> Option<()> {
-        let (user, role) = self.user_at_index(self.selected_index)?;
-        let new_role = if role == Some(ChannelRole::Admin) {
-            ChannelRole::Member
-        } else {
-            ChannelRole::Admin
-        };
+    fn set_user_role(
+        &mut self,
+        user_id: UserId,
+        new_role: ChannelRole,
+        cx: &mut ViewContext<Picker<Self>>,
+    ) -> Option<()> {
         let update = self.channel_store.update(cx, |store, cx| {
-            store.set_member_role(self.channel_id, user.id, new_role, cx)
+            store.set_member_role(self.channel_id, user_id, new_role, cx)
         });
         cx.spawn(|picker, mut cx| async move {
             update.await?;
             picker.update(&mut cx, |picker, cx| {
-                let this = picker.delegate_mut();
-                if let Some(member) = this.members.iter_mut().find(|m| m.user.id == user.id) {
+                let this = &mut picker.delegate;
+                if let Some(member) = this.members.iter_mut().find(|m| m.user.id == user_id) {
                     member.role = new_role;
                 }
                 cx.focus_self();
@@ -635,16 +464,14 @@ impl ChannelModalDelegate {
         Some(())
     }
 
-    fn remove_selected_member(&mut self, cx: &mut ViewContext<Picker<Self>>) -> Option<()> {
-        let (user, _) = self.user_at_index(self.selected_index)?;
-        let user_id = user.id;
+    fn remove_member(&mut self, user_id: UserId, cx: &mut ViewContext<Picker<Self>>) -> Option<()> {
         let update = self.channel_store.update(cx, |store, cx| {
             store.remove_member(self.channel_id, user_id, cx)
         });
         cx.spawn(|picker, mut cx| async move {
             update.await?;
             picker.update(&mut cx, |picker, cx| {
-                let this = picker.delegate_mut();
+                let this = &mut picker.delegate;
                 if let Some(ix) = this.members.iter_mut().position(|m| m.user.id == user_id) {
                     this.members.remove(ix);
                     this.matching_member_indices.retain_mut(|member_ix| {
@@ -661,7 +488,7 @@ impl ChannelModalDelegate {
                     .selected_index
                     .min(this.matching_member_indices.len().saturating_sub(1));
 
-                cx.focus_self();
+                picker.focus(cx);
                 cx.notify();
             })
         })
@@ -683,7 +510,7 @@ impl ChannelModalDelegate {
                     kind: proto::channel_member::Kind::Invitee,
                     role: ChannelRole::Member,
                 };
-                let members = &mut this.delegate_mut().members;
+                let members = &mut this.delegate.members;
                 match members.binary_search_by_key(&new_member.sort_key(), |k| k.sort_key()) {
                     Ok(ix) | Err(ix) => members.insert(ix, new_member),
                 }
@@ -694,24 +521,55 @@ impl ChannelModalDelegate {
         .detach_and_log_err(cx);
     }
 
-    fn show_context_menu(&mut self, role: ChannelRole, cx: &mut ViewContext<Picker<Self>>) {
-        self.context_menu.update(cx, |context_menu, cx| {
-            context_menu.show(
-                Default::default(),
-                AnchorCorner::TopRight,
-                vec![
-                    ContextMenuItem::action("Remove", RemoveMember),
-                    ContextMenuItem::action(
-                        if role == ChannelRole::Admin {
-                            "Make non-admin"
-                        } else {
-                            "Make admin"
-                        },
-                        ToggleMemberAdmin,
-                    ),
-                ],
-                cx,
-            )
-        })
+    fn show_context_menu(
+        &mut self,
+        user: Arc<User>,
+        role: ChannelRole,
+        cx: &mut ViewContext<Picker<Self>>,
+    ) {
+        let user_id = user.id;
+        let picker = cx.view().clone();
+        let context_menu = ContextMenu::build(cx, |mut menu, _cx| {
+            menu = menu.entry("Remove Member", None, {
+                let picker = picker.clone();
+                move |cx| {
+                    picker.update(cx, |picker, cx| {
+                        picker.delegate.remove_member(user_id, cx);
+                    })
+                }
+            });
+
+            let picker = picker.clone();
+            match role {
+                ChannelRole::Admin => {
+                    menu = menu.entry("Revoke Admin", None, move |cx| {
+                        picker.update(cx, |picker, cx| {
+                            picker
+                                .delegate
+                                .set_user_role(user_id, ChannelRole::Member, cx);
+                        })
+                    });
+                }
+                ChannelRole::Member => {
+                    menu = menu.entry("Make Admin", None, move |cx| {
+                        picker.update(cx, |picker, cx| {
+                            picker
+                                .delegate
+                                .set_user_role(user_id, ChannelRole::Admin, cx);
+                        })
+                    });
+                }
+                _ => {}
+            };
+
+            menu
+        });
+        cx.focus_view(&context_menu);
+        let subscription = cx.subscribe(&context_menu, |picker, _, _: &DismissEvent, cx| {
+            picker.delegate.context_menu = None;
+            picker.focus(cx);
+            cx.notify();
+        });
+        self.context_menu = Some((context_menu, subscription));
     }
 }

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

@@ -1,42 +1,30 @@
 use client::{ContactRequestStatus, User, UserStore};
 use gpui::{
-    elements::*, AppContext, Entity, ModelHandle, MouseState, Task, View, ViewContext, ViewHandle,
+    AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, 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;
-
-pub fn init(cx: &mut AppContext) {
-    Picker::<ContactFinderDelegate>::init(cx);
-    cx.add_action(ContactFinder::dismiss)
-}
+use theme::ActiveTheme as _;
+use ui::{prelude::*, Avatar, ListItem, ListItemSpacing};
+use util::{ResultExt as _, TryFutureExt};
+use workspace::ModalView;
 
 pub struct ContactFinder {
-    picker: ViewHandle<Picker<ContactFinderDelegate>>,
-    has_focus: bool,
+    picker: View<Picker<ContactFinderDelegate>>,
 }
 
 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.new_view(|cx| Picker::new(delegate, cx).modal(false));
 
-        Self {
-            picker,
-            has_focus: false,
-        }
+        Self { picker }
     }
 
     pub fn set_query(&mut self, query: String, cx: &mut ViewContext<Self>) {
@@ -44,101 +32,45 @@ impl ContactFinder {
             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()
-        }
-
-        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),
+impl Render for ContactFinder {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+        v_stack()
+            .elevation_3(cx)
+            .child(
+                v_stack()
+                    .px_2()
+                    .py_1()
+                    .bg(cx.theme().colors().element_background)
+                    // HACK: Prevent the background color from overflowing the parent container.
+                    .rounded_t(px(8.))
+                    .child(Label::new("Contacts"))
+                    .child(h_stack().child(Label::new("Invite new contacts"))),
             )
-            .with_child(
-                ChildView::new(&self.picker, cx)
-                    .contained()
-                    .with_style(theme.body),
-            )
-            .constrained()
-            .with_max_height(theme.max_height)
-            .with_max_width(theme.max_width)
-            .contained()
-            .with_style(theme.modal)
-            .into_any()
-    }
-
-    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;
-    }
-}
-
-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,
-        }
+            .child(self.picker.clone())
+            .w(rems(34.))
     }
 }
 
 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 ModalView for ContactFinder {}
+
+impl FocusableView for ContactFinder {
+    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+        self.picker.focus_handle(cx)
     }
+}
+
+impl PickerDelegate for ContactFinderDelegate {
+    type ListItem = ListItem;
 
     fn match_count(&self) -> usize {
         self.potential_contacts.len()
@@ -152,6 +84,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 +97,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 +127,17 @@ impl PickerDelegate for ContactFinderDelegate {
     }
 
     fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
-        cx.emit(PickerEvent::Dismiss);
+        self.parent
+            .update(cx, |_, cx| cx.emit(DismissEvent))
+            .log_err();
     }
 
     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 +148,16 @@ 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()
+        Some(
+            ListItem::new(ix)
+                .inset(true)
+                .spacing(ListItemSpacing::Sparse)
+                .selected(selected)
+                .start_slot(Avatar::new(user.avatar_uri.clone()))
+                .child(Label::new(user.github_login.clone()))
+                .end_slot::<IconElement>(
+                    icon_path.map(|icon_path| IconElement::from_path(icon_path)),
+                ),
+        )
     }
 }

crates/collab_ui/src/collab_titlebar_item.rs 🔗

@@ -1,30 +1,24 @@
-use crate::{
-    face_pile::FacePile, toggle_deafen, toggle_mute, toggle_screen_sharing, LeaveCall,
-    ToggleDeafen, ToggleMute, ToggleScreenSharing,
-};
+use crate::face_pile::FacePile;
 use auto_update::AutoUpdateStatus;
 use call::{ActiveCall, ParticipantLocation, Room};
-use client::{proto::PeerId, Client, SignIn, SignOut, User, UserStore};
-use clock::ReplicaId;
-use context_menu::{ContextMenu, ContextMenuItem};
+use client::{proto::PeerId, Client, ParticipantIndex, User, UserStore};
 use gpui::{
-    actions,
-    color::Color,
-    elements::*,
-    geometry::{rect::RectF, vector::vec2f, PathBuilder},
-    json::{self, ToJson},
-    platform::{CursorStyle, MouseButton},
-    AppContext, Entity, ImageData, ModelHandle, Subscription, View, ViewContext, ViewHandle,
-    WeakViewHandle,
+    actions, canvas, div, point, px, rems, Action, AnyElement, AppContext, Element, Hsla,
+    InteractiveElement, IntoElement, Model, ParentElement, Path, Render,
+    StatefulInteractiveElement, Styled, Subscription, View, ViewContext, VisualContext, WeakView,
+    WindowBounds,
 };
-use picker::PickerEvent;
 use project::{Project, RepositoryEntry};
-use recent_projects::{build_recent_projects, RecentProjects};
-use std::{ops::Range, sync::Arc};
-use theme::{AvatarStyle, Theme};
+use recent_projects::RecentProjects;
+use std::sync::Arc;
+use theme::{ActiveTheme, PlayerColors};
+use ui::{
+    h_stack, popover_menu, prelude::*, Avatar, Button, ButtonLike, ButtonStyle, ContextMenu, Icon,
+    IconButton, IconElement, Tooltip,
+};
 use util::ResultExt;
 use vcs_menu::{build_branch_list, BranchList, OpenRecent as ToggleVcsMenu};
-use workspace::{FollowNextCollaborator, Workspace, WORKSPACE_DB};
+use workspace::{notifications::NotifyResultExt, Workspace};
 
 const MAX_PROJECT_NAME_LENGTH: usize = 40;
 const MAX_BRANCH_NAME_LENGTH: usize = 40;
@@ -32,131 +26,269 @@ const MAX_BRANCH_NAME_LENGTH: usize = 40;
 actions!(
     collab,
     [
-        ToggleUserMenu,
-        ToggleProjectMenu,
-        SwitchBranch,
         ShareProject,
         UnshareProject,
+        ToggleUserMenu,
+        ToggleProjectMenu,
+        SwitchBranch
     ]
 );
 
 pub fn init(cx: &mut AppContext) {
-    cx.add_action(CollabTitlebarItem::share_project);
-    cx.add_action(CollabTitlebarItem::unshare_project);
-    cx.add_action(CollabTitlebarItem::toggle_user_menu);
-    cx.add_action(CollabTitlebarItem::toggle_vcs_menu);
-    cx.add_action(CollabTitlebarItem::toggle_project_menu);
+    cx.observe_new_views(|workspace: &mut Workspace, cx| {
+        let titlebar_item = cx.new_view(|cx| CollabTitlebarItem::new(workspace, cx));
+        workspace.set_titlebar_item(titlebar_item.into(), cx)
+    })
+    .detach();
+    // cx.add_action(CollabTitlebarItem::share_project);
+    // cx.add_action(CollabTitlebarItem::unshare_project);
+    // cx.add_action(CollabTitlebarItem::toggle_user_menu);
+    // cx.add_action(CollabTitlebarItem::toggle_vcs_menu);
+    // cx.add_action(CollabTitlebarItem::toggle_project_menu);
 }
 
 pub struct CollabTitlebarItem {
-    project: ModelHandle<Project>,
-    user_store: ModelHandle<UserStore>,
+    project: Model<Project>,
+    user_store: Model<UserStore>,
     client: Arc<Client>,
-    workspace: WeakViewHandle<Workspace>,
-    branch_popover: Option<ViewHandle<BranchList>>,
-    project_popover: Option<ViewHandle<recent_projects::RecentProjects>>,
-    user_menu: ViewHandle<ContextMenu>,
+    workspace: WeakView<Workspace>,
     _subscriptions: Vec<Subscription>,
 }
 
-impl Entity for CollabTitlebarItem {
-    type Event = ();
-}
-
-impl View for CollabTitlebarItem {
-    fn ui_name() -> &'static str {
-        "CollabTitlebarItem"
-    }
-
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let workspace = if let Some(workspace) = self.workspace.upgrade(cx) {
-            workspace
-        } else {
-            return Empty::new().into_any();
-        };
-
-        let theme = theme::current(cx).clone();
-        let mut left_container = Flex::row();
-        let mut right_container = Flex::row().align_children_center();
-
-        left_container.add_child(self.collect_title_root_names(theme.clone(), cx));
-
-        let user = self.user_store.read(cx).current_user();
-        let peer_id = self.client.peer_id();
-        if let Some(((user, peer_id), room)) = user
-            .as_ref()
-            .zip(peer_id)
-            .zip(ActiveCall::global(cx).read(cx).room().cloned())
-        {
-            if room.read(cx).can_publish() {
-                right_container
-                    .add_children(self.render_in_call_share_unshare_button(&workspace, &theme, cx));
-            }
-            right_container.add_child(self.render_leave_call(&theme, cx));
-            let muted = room.read(cx).is_muted(cx);
-            let speaking = room.read(cx).is_speaking();
-            left_container.add_child(
-                self.render_current_user(&workspace, &theme, &user, peer_id, muted, speaking, cx),
-            );
-            left_container.add_children(self.render_collaborators(&workspace, &theme, &room, cx));
-            if room.read(cx).can_publish() {
-                right_container.add_child(self.render_toggle_mute(&theme, &room, cx));
-            }
-            right_container.add_child(self.render_toggle_deafen(&theme, &room, cx));
-            if room.read(cx).can_publish() {
-                right_container
-                    .add_child(self.render_toggle_screen_sharing_button(&theme, &room, cx));
-            }
-        }
-
-        let status = workspace.read(cx).client().status();
-        let status = &*status.borrow();
-        if matches!(status, client::Status::Connected { .. }) {
-            let avatar = user.as_ref().and_then(|user| user.avatar.clone());
-            right_container.add_child(self.render_user_menu_button(&theme, avatar, cx));
-        } else {
-            right_container.add_children(self.render_connection_status(status, cx));
-            right_container.add_child(self.render_sign_in_button(&theme, cx));
-            right_container.add_child(self.render_user_menu_button(&theme, None, cx));
-        }
-
-        Stack::new()
-            .with_child(left_container)
-            .with_child(
-                Flex::row()
-                    .with_child(
-                        right_container.contained().with_background_color(
-                            theme
-                                .titlebar
-                                .container
-                                .background_color
-                                .unwrap_or_else(|| Color::transparent_black()),
-                        ),
-                    )
-                    .aligned()
-                    .right(),
+impl Render for CollabTitlebarItem {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+        let room = ActiveCall::global(cx).read(cx).room().cloned();
+        let current_user = self.user_store.read(cx).current_user();
+        let client = self.client.clone();
+        let project_id = self.project.read(cx).remote_id();
+
+        h_stack()
+            .id("titlebar")
+            .justify_between()
+            .w_full()
+            .h(rems(1.75))
+            // Set a non-scaling min-height here to ensure the titlebar is
+            // always at least the height of the traffic lights.
+            .min_h(px(32.))
+            .map(|this| {
+                if matches!(cx.window_bounds(), WindowBounds::Fullscreen) {
+                    this.pl_2()
+                } else {
+                    // Use pixels here instead of a rem-based size because the macOS traffic
+                    // lights are a static size, and don't scale with the rest of the UI.
+                    this.pl(px(80.))
+                }
+            })
+            .bg(cx.theme().colors().title_bar_background)
+            .on_click(|event, cx| {
+                if event.up.click_count == 2 {
+                    cx.zoom_window();
+                }
+            })
+            // left side
+            .child(
+                h_stack()
+                    .gap_1()
+                    .children(self.render_project_host(cx))
+                    .child(self.render_project_name(cx))
+                    .children(self.render_project_branch(cx))
+                    .when_some(
+                        current_user.clone().zip(client.peer_id()).zip(room.clone()),
+                        |this, ((current_user, peer_id), room)| {
+                            let player_colors = cx.theme().players();
+                            let room = room.read(cx);
+                            let mut remote_participants =
+                                room.remote_participants().values().collect::<Vec<_>>();
+                            remote_participants.sort_by_key(|p| p.participant_index.0);
+
+                            this.children(self.render_collaborator(
+                                &current_user,
+                                peer_id,
+                                true,
+                                room.is_speaking(),
+                                room.is_muted(cx),
+                                &room,
+                                project_id,
+                                &current_user,
+                            ))
+                            .children(
+                                remote_participants.iter().filter_map(|collaborator| {
+                                    let is_present = project_id.map_or(false, |project_id| {
+                                        collaborator.location
+                                            == ParticipantLocation::SharedProject { project_id }
+                                    });
+
+                                    let face_pile = self.render_collaborator(
+                                        &collaborator.user,
+                                        collaborator.peer_id,
+                                        is_present,
+                                        collaborator.speaking,
+                                        collaborator.muted,
+                                        &room,
+                                        project_id,
+                                        &current_user,
+                                    )?;
+
+                                    Some(
+                                        v_stack()
+                                            .id(("collaborator", collaborator.user.id))
+                                            .child(face_pile)
+                                            .child(render_color_ribbon(
+                                                collaborator.participant_index,
+                                                player_colors,
+                                            ))
+                                            .cursor_pointer()
+                                            .on_click({
+                                                let peer_id = collaborator.peer_id;
+                                                cx.listener(move |this, _, cx| {
+                                                    this.workspace
+                                                        .update(cx, |workspace, cx| {
+                                                            workspace.follow(peer_id, cx);
+                                                        })
+                                                        .ok();
+                                                })
+                                            })
+                                            .tooltip({
+                                                let login = collaborator.user.github_login.clone();
+                                                move |cx| {
+                                                    Tooltip::text(format!("Follow {login}"), cx)
+                                                }
+                                            }),
+                                    )
+                                }),
+                            )
+                        },
+                    ),
+            )
+            // right side
+            .child(
+                h_stack()
+                    .gap_1()
+                    .pr_1()
+                    .when_some(room, |this, room| {
+                        let room = room.read(cx);
+                        let project = self.project.read(cx);
+                        let is_local = project.is_local();
+                        let is_shared = is_local && project.is_shared();
+                        let is_muted = room.is_muted(cx);
+                        let is_deafened = room.is_deafened().unwrap_or(false);
+                        let is_screen_sharing = room.is_screen_sharing();
+
+                        this.when(is_local, |this| {
+                            this.child(
+                                Button::new(
+                                    "toggle_sharing",
+                                    if is_shared { "Unshare" } else { "Share" },
+                                )
+                                .style(ButtonStyle::Subtle)
+                                .label_size(LabelSize::Small)
+                                .on_click(cx.listener(
+                                    move |this, _, cx| {
+                                        if is_shared {
+                                            this.unshare_project(&Default::default(), cx);
+                                        } else {
+                                            this.share_project(&Default::default(), cx);
+                                        }
+                                    },
+                                )),
+                            )
+                        })
+                        .child(
+                            IconButton::new("leave-call", ui::Icon::Exit)
+                                .style(ButtonStyle::Subtle)
+                                .icon_size(IconSize::Small)
+                                .on_click(move |_, cx| {
+                                    ActiveCall::global(cx)
+                                        .update(cx, |call, cx| call.hang_up(cx))
+                                        .detach_and_log_err(cx);
+                                }),
+                        )
+                        .child(
+                            IconButton::new(
+                                "mute-microphone",
+                                if is_muted {
+                                    ui::Icon::MicMute
+                                } else {
+                                    ui::Icon::Mic
+                                },
+                            )
+                            .style(ButtonStyle::Subtle)
+                            .icon_size(IconSize::Small)
+                            .selected(is_muted)
+                            .on_click(move |_, cx| crate::toggle_mute(&Default::default(), cx)),
+                        )
+                        .child(
+                            IconButton::new(
+                                "mute-sound",
+                                if is_deafened {
+                                    ui::Icon::AudioOff
+                                } else {
+                                    ui::Icon::AudioOn
+                                },
+                            )
+                            .style(ButtonStyle::Subtle)
+                            .icon_size(IconSize::Small)
+                            .selected(is_deafened)
+                            .tooltip(move |cx| {
+                                Tooltip::with_meta("Deafen Audio", None, "Mic will be muted", cx)
+                            })
+                            .on_click(move |_, cx| crate::toggle_mute(&Default::default(), cx)),
+                        )
+                        .child(
+                            IconButton::new("screen-share", ui::Icon::Screen)
+                                .style(ButtonStyle::Subtle)
+                                .icon_size(IconSize::Small)
+                                .selected(is_screen_sharing)
+                                .on_click(move |_, cx| {
+                                    crate::toggle_screen_sharing(&Default::default(), cx)
+                                }),
+                        )
+                    })
+                    .map(|el| {
+                        let status = self.client.status();
+                        let status = &*status.borrow();
+                        if matches!(status, client::Status::Connected { .. }) {
+                            el.child(self.render_user_menu_button(cx))
+                        } else {
+                            el.children(self.render_connection_status(status, cx))
+                                .child(self.render_sign_in_button(cx))
+                                .child(self.render_user_menu_button(cx))
+                        }
+                    }),
             )
-            .into_any()
     }
 }
 
+fn render_color_ribbon(participant_index: ParticipantIndex, colors: &PlayerColors) -> gpui::Canvas {
+    let color = colors.color_for_participant(participant_index.0).cursor;
+    canvas(move |bounds, cx| {
+        let mut path = Path::new(bounds.lower_left());
+        let height = bounds.size.height;
+        path.curve_to(bounds.origin + point(height, px(0.)), bounds.origin);
+        path.line_to(bounds.upper_right() - point(height, px(0.)));
+        path.curve_to(bounds.lower_right(), bounds.upper_right());
+        path.line_to(bounds.lower_left());
+        cx.paint_path(path, color);
+    })
+    .h_1()
+    .w_full()
+}
+
 impl CollabTitlebarItem {
-    pub fn new(
-        workspace: &Workspace,
-        workspace_handle: &ViewHandle<Workspace>,
-        cx: &mut ViewContext<Self>,
-    ) -> Self {
+    pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
         let project = workspace.project().clone();
         let user_store = workspace.app_state().user_store.clone();
         let client = workspace.app_state().client.clone();
         let active_call = ActiveCall::global(cx);
         let mut subscriptions = Vec::new();
-        subscriptions.push(cx.observe(workspace_handle, |_, _, cx| cx.notify()));
+        subscriptions.push(
+            cx.observe(&workspace.weak_handle().upgrade().unwrap(), |_, _, cx| {
+                cx.notify()
+            }),
+        );
         subscriptions.push(cx.observe(&project, |_, _, cx| cx.notify()));
         subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx)));
-        subscriptions.push(cx.observe_window_activation(|this, active, cx| {
-            this.window_activation_changed(active, cx)
-        }));
+        subscriptions.push(cx.observe_window_activation(Self::window_activation_changed));
         subscriptions.push(cx.observe(&user_store, |_, _, cx| cx.notify()));
 
         Self {
@@ -164,184 +296,132 @@ impl CollabTitlebarItem {
             project,
             user_store,
             client,
-            user_menu: cx.add_view(|cx| {
-                let view_id = cx.view_id();
-                let mut menu = ContextMenu::new(view_id, cx);
-                menu.set_position_mode(OverlayPositionMode::Local);
-                menu
-            }),
-            branch_popover: None,
-            project_popover: None,
             _subscriptions: subscriptions,
         }
     }
 
-    fn collect_title_root_names(
-        &self,
-        theme: Arc<Theme>,
-        cx: &mut ViewContext<Self>,
-    ) -> AnyElement<Self> {
-        let project = self.project.read(cx);
+    // resolve if you are in a room -> render_project_owner
+    // render_project_owner -> resolve if you are in a room -> Option<foo>
 
-        let (name, entry) = {
-            let mut names_and_branches = project.visible_worktrees(cx).map(|worktree| {
+    pub fn render_project_host(&self, cx: &mut ViewContext<Self>) -> Option<impl Element> {
+        let host = self.project.read(cx).host()?;
+        let host = self.user_store.read(cx).get_cached_user(host.user_id)?;
+        let participant_index = self
+            .user_store
+            .read(cx)
+            .participant_indices()
+            .get(&host.id)?;
+        Some(
+            div().border().border_color(gpui::red()).child(
+                Button::new("project_owner_trigger", host.github_login.clone())
+                    .color(Color::Player(participant_index.0))
+                    .style(ButtonStyle::Subtle)
+                    .label_size(LabelSize::Small)
+                    .tooltip(move |cx| Tooltip::text("Toggle following", cx)),
+            ),
+        )
+    }
+
+    pub fn render_project_name(&self, cx: &mut ViewContext<Self>) -> impl Element {
+        let name = {
+            let mut names = self.project.read(cx).visible_worktrees(cx).map(|worktree| {
                 let worktree = worktree.read(cx);
-                (worktree.root_name(), worktree.root_git_entry())
+                worktree.root_name()
             });
 
-            names_and_branches.next().unwrap_or(("", None))
+            names.next().unwrap_or("")
         };
 
         let name = util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH);
-        let branch_prepended = entry
-            .as_ref()
-            .and_then(RepositoryEntry::branch)
-            .map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH));
-        let project_style = theme.titlebar.project_menu_button.clone();
-        let git_style = theme.titlebar.git_menu_button.clone();
-        let item_spacing = theme.titlebar.item_spacing;
-
-        let mut ret = Flex::row();
+        let workspace = self.workspace.clone();
+        popover_menu("project_name_trigger")
+            .trigger(
+                Button::new("project_name_trigger", name)
+                    .style(ButtonStyle::Subtle)
+                    .label_size(LabelSize::Small)
+                    .tooltip(move |cx| Tooltip::text("Recent Projects", cx)),
+            )
+            .menu(move |cx| Some(Self::render_project_popover(workspace.clone(), cx)))
+    }
 
-        if let Some(project_host) = self.collect_project_host(theme.clone(), cx) {
-            ret = ret.with_child(project_host)
-        }
+    pub fn render_project_branch(&self, cx: &mut ViewContext<Self>) -> Option<impl Element> {
+        let entry = {
+            let mut names_and_branches =
+                self.project.read(cx).visible_worktrees(cx).map(|worktree| {
+                    let worktree = worktree.read(cx);
+                    worktree.root_git_entry()
+                });
 
-        ret = ret.with_child(
-            Stack::new()
-                .with_child(
-                    MouseEventHandler::new::<ToggleProjectMenu, _>(0, cx, |mouse_state, cx| {
-                        let style = project_style
-                            .in_state(self.project_popover.is_some())
-                            .style_for(mouse_state);
-                        enum RecentProjectsTooltip {}
-                        Label::new(name, style.text.clone())
-                            .contained()
-                            .with_style(style.container)
-                            .aligned()
-                            .left()
-                            .with_tooltip::<RecentProjectsTooltip>(
-                                0,
-                                "Recent projects",
-                                Some(Box::new(recent_projects::OpenRecent)),
-                                theme.tooltip.clone(),
+            names_and_branches.next().flatten()
+        };
+        let workspace = self.workspace.upgrade()?;
+        let branch_name = entry
+            .as_ref()
+            .and_then(RepositoryEntry::branch)
+            .map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH))?;
+        Some(
+            popover_menu("project_branch_trigger")
+                .trigger(
+                    Button::new("project_branch_trigger", branch_name)
+                        .color(Color::Muted)
+                        .style(ButtonStyle::Subtle)
+                        .label_size(LabelSize::Small)
+                        .tooltip(move |cx| {
+                            Tooltip::with_meta(
+                                "Recent Branches",
+                                Some(&ToggleVcsMenu),
+                                "Local branches only",
                                 cx,
                             )
-                            .into_any_named("title-project-name")
-                    })
-                    .with_cursor_style(CursorStyle::PointingHand)
-                    .on_down(MouseButton::Left, move |_, this, cx| {
-                        this.toggle_project_menu(&Default::default(), cx)
-                    })
-                    .on_click(MouseButton::Left, move |_, _, _| {}),
+                        }),
                 )
-                .with_children(self.render_project_popover_host(&theme.titlebar, cx)),
-        );
-        if let Some(git_branch) = branch_prepended {
-            ret = ret.with_child(
-                Flex::row().with_child(
-                    Stack::new()
-                        .with_child(
-                            MouseEventHandler::new::<ToggleVcsMenu, _>(0, cx, |mouse_state, cx| {
-                                enum BranchPopoverTooltip {}
-                                let style = git_style
-                                    .in_state(self.branch_popover.is_some())
-                                    .style_for(mouse_state);
-                                Label::new(git_branch, style.text.clone())
-                                    .contained()
-                                    .with_style(style.container.clone())
-                                    .with_margin_right(item_spacing)
-                                    .aligned()
-                                    .left()
-                                    .with_tooltip::<BranchPopoverTooltip>(
-                                        0,
-                                        "Recent branches",
-                                        Some(Box::new(ToggleVcsMenu)),
-                                        theme.tooltip.clone(),
-                                        cx,
-                                    )
-                                    .into_any_named("title-project-branch")
-                            })
-                            .with_cursor_style(CursorStyle::PointingHand)
-                            .on_down(MouseButton::Left, move |_, this, cx| {
-                                this.toggle_vcs_menu(&Default::default(), cx)
-                            })
-                            .on_click(MouseButton::Left, move |_, _, _| {}),
-                        )
-                        .with_children(self.render_branches_popover_host(&theme.titlebar, cx)),
-                ),
-            )
-        }
-        ret.into_any()
+                .menu(move |cx| Self::render_vcs_popover(workspace.clone(), cx)),
+        )
     }
 
-    fn collect_project_host(
+    fn render_collaborator(
         &self,
-        theme: Arc<Theme>,
-        cx: &mut ViewContext<Self>,
-    ) -> Option<AnyElement<Self>> {
-        if ActiveCall::global(cx).read(cx).room().is_none() {
-            return None;
-        }
-        let project = self.project.read(cx);
-        let user_store = self.user_store.read(cx);
-
-        if project.is_local() {
-            return None;
-        }
-
-        let Some(host) = project.host() else {
-            return None;
-        };
-        let (Some(host_user), Some(participant_index)) = (
-            user_store.get_cached_user(host.user_id),
-            user_store.participant_indices().get(&host.user_id),
-        ) else {
-            return None;
-        };
-
-        enum ProjectHost {}
-        enum ProjectHostTooltip {}
+        user: &Arc<User>,
+        peer_id: PeerId,
+        is_present: bool,
+        is_speaking: bool,
+        is_muted: bool,
+        room: &Room,
+        project_id: Option<u64>,
+        current_user: &Arc<User>,
+    ) -> Option<FacePile> {
+        let followers = project_id.map_or(&[] as &[_], |id| room.followers_for(peer_id, id));
+
+        let pile = FacePile::default()
+            .child(
+                Avatar::new(user.avatar_uri.clone())
+                    .grayscale(!is_present)
+                    .border_color(if is_speaking {
+                        gpui::blue()
+                    } else if is_muted {
+                        gpui::red()
+                    } else {
+                        Hsla::default()
+                    }),
+            )
+            .children(followers.iter().filter_map(|follower_peer_id| {
+                let follower = room
+                    .remote_participants()
+                    .values()
+                    .find_map(|p| (p.peer_id == *follower_peer_id).then_some(&p.user))
+                    .or_else(|| {
+                        (self.client.peer_id() == Some(*follower_peer_id)).then_some(current_user)
+                    })?
+                    .clone();
 
-        let host_style = theme.titlebar.project_host.clone();
-        let selection_style = theme
-            .editor
-            .selection_style_for_room_participant(participant_index.0);
-        let peer_id = host.peer_id.clone();
+                Some(Avatar::new(follower.avatar_uri.clone()))
+            }));
 
-        Some(
-            MouseEventHandler::new::<ProjectHost, _>(0, cx, |mouse_state, _| {
-                let mut host_style = host_style.style_for(mouse_state).clone();
-                host_style.text.color = selection_style.cursor;
-                Label::new(host_user.github_login.clone(), host_style.text)
-                    .contained()
-                    .with_style(host_style.container)
-                    .aligned()
-                    .left()
-            })
-            .with_cursor_style(CursorStyle::PointingHand)
-            .on_click(MouseButton::Left, move |_, this, cx| {
-                if let Some(workspace) = this.workspace.upgrade(cx) {
-                    if let Some(task) =
-                        workspace.update(cx, |workspace, cx| workspace.follow(peer_id, cx))
-                    {
-                        task.detach_and_log_err(cx);
-                    }
-                }
-            })
-            .with_tooltip::<ProjectHostTooltip>(
-                0,
-                host_user.github_login.clone() + " is sharing this project. Click to follow.",
-                None,
-                theme.tooltip.clone(),
-                cx,
-            )
-            .into_any_named("project-host"),
-        )
+        Some(pile)
     }
 
-    fn window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
-        let project = if active {
+    fn window_activation_changed(&mut self, cx: &mut ViewContext<Self>) {
+        let project = if cx.is_window_active() {
             Some(self.project.clone())
         } else {
             None
@@ -371,801 +451,44 @@ impl CollabTitlebarItem {
             .log_err();
     }
 
-    pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext<Self>) {
-        self.user_menu.update(cx, |user_menu, cx| {
-            let items = if let Some(_) = self.user_store.read(cx).current_user() {
-                vec![
-                    ContextMenuItem::action("Settings", zed_actions::OpenSettings),
-                    ContextMenuItem::action("Theme", theme_selector::Toggle),
-                    ContextMenuItem::separator(),
-                    ContextMenuItem::action(
-                        "Share Feedback",
-                        feedback::feedback_editor::GiveFeedback,
-                    ),
-                    ContextMenuItem::action("Sign Out", SignOut),
-                ]
-            } else {
-                vec![
-                    ContextMenuItem::action("Settings", zed_actions::OpenSettings),
-                    ContextMenuItem::action("Theme", theme_selector::Toggle),
-                    ContextMenuItem::separator(),
-                    ContextMenuItem::action(
-                        "Share Feedback",
-                        feedback::feedback_editor::GiveFeedback,
-                    ),
-                ]
-            };
-            user_menu.toggle(Default::default(), AnchorCorner::TopRight, items, cx);
-        });
-    }
-
-    fn render_branches_popover_host<'a>(
-        &'a self,
-        _theme: &'a theme::Titlebar,
-        cx: &'a mut ViewContext<Self>,
-    ) -> Option<AnyElement<Self>> {
-        self.branch_popover.as_ref().map(|child| {
-            let theme = theme::current(cx).clone();
-            let child = ChildView::new(child, cx);
-            let child = MouseEventHandler::new::<BranchList, _>(0, cx, |_, _| {
-                child
-                    .flex(1., true)
-                    .contained()
-                    .constrained()
-                    .with_width(theme.titlebar.menu.width)
-                    .with_height(theme.titlebar.menu.height)
-            })
-            .on_click(MouseButton::Left, |_, _, _| {})
-            .on_down_out(MouseButton::Left, move |_, this, cx| {
-                this.branch_popover.take();
-                cx.emit(());
-                cx.notify();
-            })
-            .contained()
-            .into_any();
-
-            Overlay::new(child)
-                .with_fit_mode(OverlayFitMode::SwitchAnchor)
-                .with_anchor_corner(AnchorCorner::TopLeft)
-                .with_z_index(999)
-                .aligned()
-                .bottom()
-                .left()
-                .into_any()
-        })
-    }
-
-    fn render_project_popover_host<'a>(
-        &'a self,
-        _theme: &'a theme::Titlebar,
-        cx: &'a mut ViewContext<Self>,
-    ) -> Option<AnyElement<Self>> {
-        self.project_popover.as_ref().map(|child| {
-            let theme = theme::current(cx).clone();
-            let child = ChildView::new(child, cx);
-            let child = MouseEventHandler::new::<RecentProjects, _>(0, cx, |_, _| {
-                child
-                    .flex(1., true)
-                    .contained()
-                    .constrained()
-                    .with_width(theme.titlebar.menu.width)
-                    .with_height(theme.titlebar.menu.height)
-            })
-            .on_click(MouseButton::Left, |_, _, _| {})
-            .on_down_out(MouseButton::Left, move |_, this, cx| {
-                this.project_popover.take();
-                cx.emit(());
-                cx.notify();
-            })
-            .into_any();
-
-            Overlay::new(child)
-                .with_fit_mode(OverlayFitMode::SwitchAnchor)
-                .with_anchor_corner(AnchorCorner::TopLeft)
-                .with_z_index(999)
-                .aligned()
-                .bottom()
-                .left()
-                .into_any()
-        })
-    }
-
-    pub fn toggle_vcs_menu(&mut self, _: &ToggleVcsMenu, cx: &mut ViewContext<Self>) {
-        if self.branch_popover.take().is_none() {
-            if let Some(workspace) = self.workspace.upgrade(cx) {
-                let Some(view) =
-                    cx.add_option_view(|cx| build_branch_list(workspace, cx).log_err())
-                else {
-                    return;
-                };
-                cx.subscribe(&view, |this, _, event, cx| {
-                    match event {
-                        PickerEvent::Dismiss => {
-                            this.branch_popover = None;
-                        }
-                    }
-
-                    cx.notify();
-                })
-                .detach();
-                self.project_popover.take();
-                cx.focus(&view);
-                self.branch_popover = Some(view);
-            }
-        }
-
-        cx.notify();
+    pub fn render_vcs_popover(
+        workspace: View<Workspace>,
+        cx: &mut WindowContext<'_>,
+    ) -> Option<View<BranchList>> {
+        let view = build_branch_list(workspace, cx).log_err()?;
+        let focus_handle = view.focus_handle(cx);
+        cx.focus(&focus_handle);
+        Some(view)
     }
 
-    pub fn toggle_project_menu(&mut self, _: &ToggleProjectMenu, cx: &mut ViewContext<Self>) {
-        let workspace = self.workspace.clone();
-        if self.project_popover.take().is_none() {
-            cx.spawn(|this, mut cx| async move {
-                let workspaces = WORKSPACE_DB
-                    .recent_workspaces_on_disk()
-                    .await
-                    .unwrap_or_default()
-                    .into_iter()
-                    .map(|(_, location)| location)
-                    .collect();
-
-                let workspace = workspace.clone();
-                this.update(&mut cx, move |this, cx| {
-                    let view = cx.add_view(|cx| build_recent_projects(workspace, workspaces, cx));
-
-                    cx.subscribe(&view, |this, _, event, cx| {
-                        match event {
-                            PickerEvent::Dismiss => {
-                                this.project_popover = None;
-                            }
-                        }
+    pub fn render_project_popover(
+        workspace: WeakView<Workspace>,
+        cx: &mut WindowContext<'_>,
+    ) -> View<RecentProjects> {
+        let view = RecentProjects::open_popover(workspace, cx);
 
-                        cx.notify();
-                    })
-                    .detach();
-                    cx.focus(&view);
-                    this.branch_popover.take();
-                    this.project_popover = Some(view);
-                    cx.notify();
-                })
-                .log_err();
-            })
-            .detach();
-        }
-        cx.notify();
-    }
-
-    fn render_toggle_screen_sharing_button(
-        &self,
-        theme: &Theme,
-        room: &ModelHandle<Room>,
-        cx: &mut ViewContext<Self>,
-    ) -> AnyElement<Self> {
-        let icon;
-        let tooltip;
-        if room.read(cx).is_screen_sharing() {
-            icon = "icons/desktop.svg";
-            tooltip = "Stop Sharing Screen"
-        } else {
-            icon = "icons/desktop.svg";
-            tooltip = "Share Screen";
-        }
-
-        let active = room.read(cx).is_screen_sharing();
-        let titlebar = &theme.titlebar;
-        MouseEventHandler::new::<ToggleScreenSharing, _>(0, cx, |state, _| {
-            let style = titlebar
-                .screen_share_button
-                .in_state(active)
-                .style_for(state);
-
-            Svg::new(icon)
-                .with_color(style.color)
-                .constrained()
-                .with_width(style.icon_width)
-                .aligned()
-                .constrained()
-                .with_width(style.button_width)
-                .with_height(style.button_width)
-                .contained()
-                .with_style(style.container)
-        })
-        .with_cursor_style(CursorStyle::PointingHand)
-        .on_click(MouseButton::Left, move |_, _, cx| {
-            toggle_screen_sharing(&Default::default(), cx)
-        })
-        .with_tooltip::<ToggleScreenSharing>(
-            0,
-            tooltip,
-            Some(Box::new(ToggleScreenSharing)),
-            theme.tooltip.clone(),
-            cx,
-        )
-        .aligned()
-        .into_any()
-    }
-    fn render_toggle_mute(
-        &self,
-        theme: &Theme,
-        room: &ModelHandle<Room>,
-        cx: &mut ViewContext<Self>,
-    ) -> AnyElement<Self> {
-        let icon;
-        let tooltip;
-        let is_muted = room.read(cx).is_muted(cx);
-        if is_muted {
-            icon = "icons/mic-mute.svg";
-            tooltip = "Unmute microphone";
-        } else {
-            icon = "icons/mic.svg";
-            tooltip = "Mute microphone";
-        }
-
-        let titlebar = &theme.titlebar;
-        MouseEventHandler::new::<ToggleMute, _>(0, cx, |state, _| {
-            let style = titlebar
-                .toggle_microphone_button
-                .in_state(is_muted)
-                .style_for(state);
-            let image = Svg::new(icon)
-                .with_color(style.color)
-                .constrained()
-                .with_width(style.icon_width)
-                .aligned()
-                .constrained()
-                .with_width(style.button_width)
-                .with_height(style.button_width)
-                .contained()
-                .with_style(style.container);
-            if let Some(color) = style.container.background_color {
-                image.with_background_color(color)
-            } else {
-                image
-            }
-        })
-        .with_cursor_style(CursorStyle::PointingHand)
-        .on_click(MouseButton::Left, move |_, _, cx| {
-            toggle_mute(&Default::default(), cx)
-        })
-        .with_tooltip::<ToggleMute>(
-            0,
-            tooltip,
-            Some(Box::new(ToggleMute)),
-            theme.tooltip.clone(),
-            cx,
-        )
-        .aligned()
-        .into_any()
-    }
-    fn render_toggle_deafen(
-        &self,
-        theme: &Theme,
-        room: &ModelHandle<Room>,
-        cx: &mut ViewContext<Self>,
-    ) -> AnyElement<Self> {
-        let icon;
-        let tooltip;
-        let is_deafened = room.read(cx).is_deafened().unwrap_or(false);
-        if is_deafened {
-            icon = "icons/speaker-off.svg";
-            tooltip = "Unmute speakers";
-        } else {
-            icon = "icons/speaker-loud.svg";
-            tooltip = "Mute speakers";
-        }
-
-        let titlebar = &theme.titlebar;
-        MouseEventHandler::new::<ToggleDeafen, _>(0, cx, |state, _| {
-            let style = titlebar
-                .toggle_speakers_button
-                .in_state(is_deafened)
-                .style_for(state);
-            Svg::new(icon)
-                .with_color(style.color)
-                .constrained()
-                .with_width(style.icon_width)
-                .aligned()
-                .constrained()
-                .with_width(style.button_width)
-                .with_height(style.button_width)
-                .contained()
-                .with_style(style.container)
-        })
-        .with_cursor_style(CursorStyle::PointingHand)
-        .on_click(MouseButton::Left, move |_, _, cx| {
-            toggle_deafen(&Default::default(), cx)
-        })
-        .with_tooltip::<ToggleDeafen>(
-            0,
-            tooltip,
-            Some(Box::new(ToggleDeafen)),
-            theme.tooltip.clone(),
-            cx,
-        )
-        .aligned()
-        .into_any()
-    }
-    fn render_leave_call(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let icon = "icons/exit.svg";
-        let tooltip = "Leave call";
-
-        let titlebar = &theme.titlebar;
-        MouseEventHandler::new::<LeaveCall, _>(0, cx, |state, _| {
-            let style = titlebar.leave_call_button.style_for(state);
-            Svg::new(icon)
-                .with_color(style.color)
-                .constrained()
-                .with_width(style.icon_width)
-                .aligned()
-                .constrained()
-                .with_width(style.button_width)
-                .with_height(style.button_width)
-                .contained()
-                .with_style(style.container)
-        })
-        .with_cursor_style(CursorStyle::PointingHand)
-        .on_click(MouseButton::Left, move |_, _, cx| {
-            ActiveCall::global(cx)
-                .update(cx, |call, cx| call.hang_up(cx))
-                .detach_and_log_err(cx);
-        })
-        .with_tooltip::<LeaveCall>(
-            0,
-            tooltip,
-            Some(Box::new(LeaveCall)),
-            theme.tooltip.clone(),
-            cx,
-        )
-        .aligned()
-        .into_any()
-    }
-    fn render_in_call_share_unshare_button(
-        &self,
-        workspace: &ViewHandle<Workspace>,
-        theme: &Theme,
-        cx: &mut ViewContext<Self>,
-    ) -> Option<AnyElement<Self>> {
-        let project = workspace.read(cx).project();
-        if project.read(cx).is_remote() {
-            return None;
-        }
-
-        let is_shared = project.read(cx).is_shared();
-        let label = if is_shared { "Stop Sharing" } else { "Share" };
-        let tooltip = if is_shared {
-            "Stop sharing project with call participants"
-        } else {
-            "Share project with call participants"
-        };
-
-        let titlebar = &theme.titlebar;
-
-        enum ShareUnshare {}
-        Some(
-            Stack::new()
-                .with_child(
-                    MouseEventHandler::new::<ShareUnshare, _>(0, cx, |state, _| {
-                        //TODO: Ensure this button has consistent width for both text variations
-                        let style = titlebar.share_button.inactive_state().style_for(state);
-                        Label::new(label, style.text.clone())
-                            .contained()
-                            .with_style(style.container)
-                    })
-                    .with_cursor_style(CursorStyle::PointingHand)
-                    .on_click(MouseButton::Left, move |_, this, cx| {
-                        if is_shared {
-                            this.unshare_project(&Default::default(), cx);
-                        } else {
-                            this.share_project(&Default::default(), cx);
-                        }
-                    })
-                    .with_tooltip::<ShareUnshare>(
-                        0,
-                        tooltip.to_owned(),
-                        None,
-                        theme.tooltip.clone(),
-                        cx,
-                    ),
-                )
-                .aligned()
-                .contained()
-                .with_margin_left(theme.titlebar.item_spacing)
-                .into_any(),
-        )
-    }
-
-    fn render_user_menu_button(
-        &self,
-        theme: &Theme,
-        avatar: Option<Arc<ImageData>>,
-        cx: &mut ViewContext<Self>,
-    ) -> AnyElement<Self> {
-        let tooltip = theme.tooltip.clone();
-        let user_menu_button_style = if avatar.is_some() {
-            &theme.titlebar.user_menu.user_menu_button_online
-        } else {
-            &theme.titlebar.user_menu.user_menu_button_offline
-        };
-
-        let avatar_style = &user_menu_button_style.avatar;
-        Stack::new()
-            .with_child(
-                MouseEventHandler::new::<ToggleUserMenu, _>(0, cx, |state, _| {
-                    let style = user_menu_button_style
-                        .user_menu
-                        .inactive_state()
-                        .style_for(state);
-
-                    let mut dropdown = Flex::row().align_children_center();
-
-                    if let Some(avatar_img) = avatar {
-                        dropdown = dropdown.with_child(Self::render_face(
-                            avatar_img,
-                            *avatar_style,
-                            Color::transparent_black(),
-                            None,
-                        ));
-                    };
-
-                    dropdown
-                        .with_child(
-                            Svg::new("icons/caret_down.svg")
-                                .with_color(user_menu_button_style.icon.color)
-                                .constrained()
-                                .with_width(user_menu_button_style.icon.width)
-                                .contained()
-                                .into_any(),
-                        )
-                        .aligned()
-                        .constrained()
-                        .with_height(style.width)
-                        .contained()
-                        .with_style(style.container)
-                        .into_any()
-                })
-                .with_cursor_style(CursorStyle::PointingHand)
-                .on_down(MouseButton::Left, move |_, this, cx| {
-                    this.user_menu.update(cx, |menu, _| menu.delay_cancel());
-                })
-                .on_click(MouseButton::Left, move |_, this, cx| {
-                    this.toggle_user_menu(&Default::default(), cx)
-                })
-                .with_tooltip::<ToggleUserMenu>(
-                    0,
-                    "Toggle User Menu".to_owned(),
-                    Some(Box::new(ToggleUserMenu)),
-                    tooltip,
-                    cx,
-                )
-                .contained(),
-            )
-            .with_child(
-                ChildView::new(&self.user_menu, cx)
-                    .aligned()
-                    .bottom()
-                    .right(),
-            )
-            .into_any()
-    }
-
-    fn render_sign_in_button(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let titlebar = &theme.titlebar;
-        MouseEventHandler::new::<SignIn, _>(0, cx, |state, _| {
-            let style = titlebar.sign_in_button.inactive_state().style_for(state);
-            Label::new("Sign In", style.text.clone())
-                .contained()
-                .with_style(style.container)
-        })
-        .with_cursor_style(CursorStyle::PointingHand)
-        .on_click(MouseButton::Left, move |_, this, cx| {
-            let client = this.client.clone();
-            cx.app_context()
-                .spawn(|cx| async move { client.authenticate_and_connect(true, &cx).await })
-                .detach_and_log_err(cx);
-        })
-        .into_any()
-    }
-
-    fn render_collaborators(
-        &self,
-        workspace: &ViewHandle<Workspace>,
-        theme: &Theme,
-        room: &ModelHandle<Room>,
-        cx: &mut ViewContext<Self>,
-    ) -> Vec<Container<Self>> {
-        let mut participants = room
-            .read(cx)
-            .remote_participants()
-            .values()
-            .cloned()
-            .collect::<Vec<_>>();
-        participants.sort_by_cached_key(|p| p.user.github_login.clone());
-
-        participants
-            .into_iter()
-            .filter_map(|participant| {
-                let project = workspace.read(cx).project().read(cx);
-                let replica_id = project
-                    .collaborators()
-                    .get(&participant.peer_id)
-                    .map(|collaborator| collaborator.replica_id);
-                let user = participant.user.clone();
-                Some(
-                    Container::new(self.render_face_pile(
-                        &user,
-                        replica_id,
-                        participant.peer_id,
-                        Some(participant.location),
-                        participant.muted,
-                        participant.speaking,
-                        workspace,
-                        theme,
-                        cx,
-                    ))
-                    .with_margin_right(theme.titlebar.face_pile_spacing),
-                )
-            })
-            .collect()
-    }
-
-    fn render_current_user(
-        &self,
-        workspace: &ViewHandle<Workspace>,
-        theme: &Theme,
-        user: &Arc<User>,
-        peer_id: PeerId,
-        muted: bool,
-        speaking: bool,
-        cx: &mut ViewContext<Self>,
-    ) -> AnyElement<Self> {
-        let replica_id = workspace.read(cx).project().read(cx).replica_id();
-
-        Container::new(self.render_face_pile(
-            user,
-            Some(replica_id),
-            peer_id,
-            None,
-            muted,
-            speaking,
-            workspace,
-            theme,
-            cx,
-        ))
-        .with_margin_right(theme.titlebar.item_spacing)
-        .into_any()
-    }
-
-    fn render_face_pile(
-        &self,
-        user: &User,
-        _replica_id: Option<ReplicaId>,
-        peer_id: PeerId,
-        location: Option<ParticipantLocation>,
-        muted: bool,
-        speaking: bool,
-        workspace: &ViewHandle<Workspace>,
-        theme: &Theme,
-        cx: &mut ViewContext<Self>,
-    ) -> AnyElement<Self> {
-        let user_id = user.id;
-        let project_id = workspace.read(cx).project().read(cx).remote_id();
-        let room = ActiveCall::global(cx).read(cx).room().cloned();
-        let self_peer_id = workspace.read(cx).client().peer_id();
-        let self_following = workspace.read(cx).is_being_followed(peer_id);
-        let self_following_initialized = self_following
-            && room.as_ref().map_or(false, |room| match project_id {
-                None => true,
-                Some(project_id) => room
-                    .read(cx)
-                    .followers_for(peer_id, project_id)
-                    .iter()
-                    .any(|&follower| Some(follower) == self_peer_id),
-            });
-
-        let leader_style = theme.titlebar.leader_avatar;
-        let follower_style = theme.titlebar.follower_avatar;
-
-        let microphone_state = if muted {
-            Some(theme.titlebar.muted)
-        } else if speaking {
-            Some(theme.titlebar.speaking)
-        } else {
-            None
-        };
-
-        let mut background_color = theme
-            .titlebar
-            .container
-            .background_color
-            .unwrap_or_default();
-
-        let participant_index = self
-            .user_store
-            .read(cx)
-            .participant_indices()
-            .get(&user_id)
-            .copied();
-        if let Some(participant_index) = participant_index {
-            if self_following_initialized {
-                let selection = theme
-                    .editor
-                    .selection_style_for_room_participant(participant_index.0)
-                    .selection;
-                background_color = Color::blend(selection, background_color);
-                background_color.a = 255;
-            }
-        }
-
-        enum TitlebarParticipant {}
-
-        let content = MouseEventHandler::new::<TitlebarParticipant, _>(
-            peer_id.as_u64() as usize,
-            cx,
-            move |_, cx| {
-                Stack::new()
-                    .with_children(user.avatar.as_ref().map(|avatar| {
-                        let face_pile = FacePile::new(theme.titlebar.follower_avatar_overlap)
-                            .with_child(Self::render_face(
-                                avatar.clone(),
-                                Self::location_style(workspace, location, leader_style, cx),
-                                background_color,
-                                microphone_state,
-                            ))
-                            .with_children(
-                                (|| {
-                                    let project_id = project_id?;
-                                    let room = room?.read(cx);
-                                    let followers = room.followers_for(peer_id, project_id);
-                                    Some(followers.into_iter().filter_map(|&follower| {
-                                        if Some(follower) == self_peer_id {
-                                            return None;
-                                        }
-                                        let participant =
-                                            room.remote_participant_for_peer_id(follower)?;
-                                        Some(Self::render_face(
-                                            participant.user.avatar.clone()?,
-                                            follower_style,
-                                            background_color,
-                                            None,
-                                        ))
-                                    }))
-                                })()
-                                .into_iter()
-                                .flatten(),
-                            )
-                            .with_children(
-                                self_following_initialized
-                                    .then(|| self.user_store.read(cx).current_user())
-                                    .and_then(|user| {
-                                        Some(Self::render_face(
-                                            user?.avatar.clone()?,
-                                            follower_style,
-                                            background_color,
-                                            None,
-                                        ))
-                                    }),
-                            );
-
-                        let mut container = face_pile
-                            .contained()
-                            .with_style(theme.titlebar.leader_selection);
-
-                        if let Some(participant_index) = participant_index {
-                            if self_following_initialized {
-                                let color = theme
-                                    .editor
-                                    .selection_style_for_room_participant(participant_index.0)
-                                    .selection;
-                                container = container.with_background_color(color);
-                            }
-                        }
-
-                        container
-                    }))
-                    .with_children((|| {
-                        let participant_index = participant_index?;
-                        let color = theme
-                            .editor
-                            .selection_style_for_room_participant(participant_index.0)
-                            .cursor;
-                        Some(
-                            AvatarRibbon::new(color)
-                                .constrained()
-                                .with_width(theme.titlebar.avatar_ribbon.width)
-                                .with_height(theme.titlebar.avatar_ribbon.height)
-                                .aligned()
-                                .bottom(),
-                        )
-                    })())
-            },
-        );
-
-        if Some(peer_id) == self_peer_id {
-            return content.into_any();
-        }
-
-        content
-            .with_cursor_style(CursorStyle::PointingHand)
-            .on_click(MouseButton::Left, move |_, this, cx| {
-                let Some(workspace) = this.workspace.upgrade(cx) else {
-                    return;
-                };
-                if let Some(task) =
-                    workspace.update(cx, |workspace, cx| workspace.follow(peer_id, cx))
-                {
-                    task.detach_and_log_err(cx);
-                }
-            })
-            .with_tooltip::<TitlebarParticipant>(
-                peer_id.as_u64() as usize,
-                format!("Follow {}", user.github_login),
-                Some(Box::new(FollowNextCollaborator)),
-                theme.tooltip.clone(),
-                cx,
-            )
-            .into_any()
-    }
-
-    fn location_style(
-        workspace: &ViewHandle<Workspace>,
-        location: Option<ParticipantLocation>,
-        mut style: AvatarStyle,
-        cx: &ViewContext<Self>,
-    ) -> AvatarStyle {
-        if let Some(location) = location {
-            if let ParticipantLocation::SharedProject { project_id } = location {
-                if Some(project_id) != workspace.read(cx).project().read(cx).remote_id() {
-                    style.image.grayscale = true;
-                }
-            } else {
-                style.image.grayscale = true;
-            }
-        }
-
-        style
-    }
-
-    fn render_face<V: 'static>(
-        avatar: Arc<ImageData>,
-        avatar_style: AvatarStyle,
-        background_color: Color,
-        microphone_state: Option<Color>,
-    ) -> AnyElement<V> {
-        Image::from_data(avatar)
-            .with_style(avatar_style.image)
-            .aligned()
-            .contained()
-            .with_background_color(microphone_state.unwrap_or(background_color))
-            .with_corner_radius(avatar_style.outer_corner_radius)
-            .constrained()
-            .with_width(avatar_style.outer_width)
-            .with_height(avatar_style.outer_width)
-            .aligned()
-            .into_any()
+        let focus_handle = view.focus_handle(cx);
+        cx.focus(&focus_handle);
+        view
     }
 
     fn render_connection_status(
         &self,
         status: &client::Status,
         cx: &mut ViewContext<Self>,
-    ) -> Option<AnyElement<Self>> {
-        enum ConnectionStatusButton {}
-
-        let theme = &theme::current(cx).clone();
+    ) -> Option<AnyElement> {
         match status {
             client::Status::ConnectionError
             | client::Status::ConnectionLost
             | client::Status::Reauthenticating { .. }
             | client::Status::Reconnecting { .. }
             | client::Status::ReconnectionError { .. } => Some(
-                Svg::new("icons/disconnected.svg")
-                    .with_color(theme.titlebar.offline_icon.color)
-                    .constrained()
-                    .with_width(theme.titlebar.offline_icon.width)
-                    .aligned()
-                    .contained()
-                    .with_style(theme.titlebar.offline_icon.container)
-                    .into_any(),
+                div()
+                    .id("disconnected")
+                    .bg(gpui::red()) // todo!() @nate
+                    .child(IconElement::new(Icon::Disconnected))
+                    .tooltip(|cx| Tooltip::text("Disconnected", cx))
+                    .into_any_element(),
             ),
             client::Status::UpgradeRequired => {
                 let auto_updater = auto_update::AutoUpdater::get(cx);

crates/collab_ui/src/collab_ui.rs 🔗

@@ -7,27 +7,22 @@ pub mod notification_panel;
 pub mod notifications;
 mod panel_settings;
 
+use std::{rc::Rc, sync::Arc};
+
 use call::{report_call_event_for_room, ActiveCall, Room};
+pub use collab_panel::CollabPanel;
+pub use collab_titlebar_item::CollabTitlebarItem;
 use feature_flags::{ChannelsAlpha, FeatureFlagAppExt};
 use gpui::{
-    actions,
-    elements::{ContainerStyle, Empty, Image},
-    geometry::{
-        rect::RectF,
-        vector::{vec2f, Vector2F},
-    },
-    platform::{Screen, WindowBounds, WindowKind, WindowOptions},
-    AnyElement, AppContext, Element, ImageData, Task,
+    actions, point, AppContext, GlobalPixels, Pixels, PlatformDisplay, Size, Task, WindowBounds,
+    WindowKind, WindowOptions,
 };
-use std::{rc::Rc, sync::Arc};
-use theme::AvatarStyle;
-use util::ResultExt;
-use workspace::AppState;
-
-pub use collab_titlebar_item::CollabTitlebarItem;
 pub use panel_settings::{
     ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings,
 };
+use settings::Settings;
+use util::ResultExt;
+use workspace::AppState;
 
 actions!(
     collab,
@@ -35,19 +30,21 @@ actions!(
 );
 
 pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
-    settings::register::<CollaborationPanelSettings>(cx);
-    settings::register::<ChatPanelSettings>(cx);
-    settings::register::<NotificationPanelSettings>(cx);
+    CollaborationPanelSettings::register(cx);
+    ChatPanelSettings::register(cx);
+    NotificationPanelSettings::register(cx);
 
     vcs_menu::init(cx);
     collab_titlebar_item::init(cx);
     collab_panel::init(cx);
+    channel_view::init(cx);
     chat_panel::init(cx);
+    notification_panel::init(cx);
     notifications::init(&app_state, cx);
 
-    cx.add_global_action(toggle_screen_sharing);
-    cx.add_global_action(toggle_mute);
-    cx.add_global_action(toggle_deafen);
+    // cx.add_global_action(toggle_screen_sharing);
+    // cx.add_global_action(toggle_mute);
+    // cx.add_global_action(toggle_deafen);
 }
 
 pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
@@ -107,58 +104,63 @@ pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) {
 }
 
 fn notification_window_options(
-    screen: Rc<dyn Screen>,
-    window_size: Vector2F,
-) -> WindowOptions<'static> {
-    const NOTIFICATION_PADDING: f32 = 16.;
+    screen: Rc<dyn PlatformDisplay>,
+    window_size: Size<Pixels>,
+) -> WindowOptions {
+    let notification_margin_width = GlobalPixels::from(16.);
+    let notification_margin_height = GlobalPixels::from(-0.) - GlobalPixels::from(48.);
+
+    let screen_bounds = screen.bounds();
+    let size: Size<GlobalPixels> = window_size.into();
 
-    let screen_bounds = screen.content_bounds();
+    // todo!() use content bounds instead of screen.bounds and get rid of magics in point's 2nd argument.
+    let bounds = gpui::Bounds::<GlobalPixels> {
+        origin: screen_bounds.upper_right()
+            - point(
+                size.width + notification_margin_width,
+                notification_margin_height,
+            ),
+        size: window_size.into(),
+    };
     WindowOptions {
-        bounds: WindowBounds::Fixed(RectF::new(
-            screen_bounds.upper_right()
-                + vec2f(
-                    -NOTIFICATION_PADDING - window_size.x(),
-                    NOTIFICATION_PADDING,
-                ),
-            window_size,
-        )),
+        bounds: WindowBounds::Fixed(bounds),
         titlebar: None,
         center: false,
         focus: false,
         show: true,
         kind: WindowKind::PopUp,
         is_movable: false,
-        screen: Some(screen),
+        display_id: Some(screen.id()),
     }
 }
 
-fn render_avatar<T: 'static>(
-    avatar: Option<Arc<ImageData>>,
-    avatar_style: &AvatarStyle,
-    container: ContainerStyle,
-) -> AnyElement<T> {
-    avatar
-        .map(|avatar| {
-            Image::from_data(avatar)
-                .with_style(avatar_style.image)
-                .aligned()
-                .contained()
-                .with_corner_radius(avatar_style.outer_corner_radius)
-                .constrained()
-                .with_width(avatar_style.outer_width)
-                .with_height(avatar_style.outer_width)
-                .into_any()
-        })
-        .unwrap_or_else(|| {
-            Empty::new()
-                .constrained()
-                .with_width(avatar_style.outer_width)
-                .into_any()
-        })
-        .contained()
-        .with_style(container)
-        .into_any()
-}
+// fn render_avatar<T: 'static>(
+//     avatar: Option<Arc<ImageData>>,
+//     avatar_style: &AvatarStyle,
+//     container: ContainerStyle,
+// ) -> AnyElement<T> {
+//     avatar
+//         .map(|avatar| {
+//             Image::from_data(avatar)
+//                 .with_style(avatar_style.image)
+//                 .aligned()
+//                 .contained()
+//                 .with_corner_radius(avatar_style.outer_corner_radius)
+//                 .constrained()
+//                 .with_width(avatar_style.outer_width)
+//                 .with_height(avatar_style.outer_width)
+//                 .into_any()
+//         })
+//         .unwrap_or_else(|| {
+//             Empty::new()
+//                 .constrained()
+//                 .with_width(avatar_style.outer_width)
+//                 .into_any()
+//         })
+//         .contained()
+//         .with_style(container)
+//         .into_any()
+// }
 
 fn is_channels_feature_enabled(cx: &gpui::WindowContext<'_>) -> bool {
     cx.is_staff() || cx.has_flag::<ChannelsAlpha>()

crates/collab_ui/src/face_pile.rs 🔗

@@ -1,113 +1,30 @@
-use std::ops::Range;
-
 use gpui::{
-    geometry::{
-        rect::RectF,
-        vector::{vec2f, Vector2F},
-    },
-    json::ToJson,
-    serde_json::{self, json},
-    AnyElement, Axis, Element, View, ViewContext,
+    div, AnyElement, ElementId, IntoElement, ParentElement, RenderOnce, Styled, WindowContext,
 };
+use smallvec::SmallVec;
 
-pub(crate) struct FacePile<V: View> {
-    overlap: f32,
-    faces: Vec<AnyElement<V>>,
+#[derive(Default, IntoElement)]
+pub struct FacePile {
+    pub faces: SmallVec<[AnyElement; 2]>,
 }
 
-impl<V: View> FacePile<V> {
-    pub fn new(overlap: f32) -> Self {
-        Self {
-            overlap,
-            faces: Vec::new(),
-        }
-    }
-}
-
-impl<V: View> Element<V> for FacePile<V> {
-    type LayoutState = ();
-    type PaintState = ();
-
-    fn layout(
-        &mut self,
-        constraint: gpui::SizeConstraint,
-        view: &mut V,
-        cx: &mut ViewContext<V>,
-    ) -> (Vector2F, Self::LayoutState) {
-        debug_assert!(constraint.max_along(Axis::Horizontal) == f32::INFINITY);
-
-        let mut width = 0.;
-        let mut max_height = 0.;
-        for face in &mut self.faces {
-            let layout = face.layout(constraint, view, cx);
-            width += layout.x();
-            max_height = f32::max(max_height, layout.y());
-        }
-        width -= self.overlap * self.faces.len().saturating_sub(1) as f32;
-
-        (
-            Vector2F::new(width, max_height.clamp(1., constraint.max.y())),
-            (),
-        )
-    }
-
-    fn paint(
-        &mut self,
-        bounds: RectF,
-        visible_bounds: RectF,
-        _layout: &mut Self::LayoutState,
-        view: &mut V,
-        cx: &mut ViewContext<V>,
-    ) -> Self::PaintState {
-        let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
-
-        let origin_y = bounds.upper_right().y();
-        let mut origin_x = bounds.upper_right().x();
-
-        for face in self.faces.iter_mut().rev() {
-            let size = face.size();
-            origin_x -= size.x();
-            let origin_y = origin_y + (bounds.height() - size.y()) / 2.0;
-
-            cx.scene().push_layer(None);
-            face.paint(vec2f(origin_x, origin_y), visible_bounds, view, cx);
-            cx.scene().pop_layer();
-            origin_x += self.overlap;
-        }
-
-        ()
-    }
-
-    fn rect_for_text_range(
-        &self,
-        _: Range<usize>,
-        _: RectF,
-        _: RectF,
-        _: &Self::LayoutState,
-        _: &Self::PaintState,
-        _: &V,
-        _: &ViewContext<V>,
-    ) -> Option<RectF> {
-        None
-    }
-
-    fn debug(
-        &self,
-        bounds: RectF,
-        _: &Self::LayoutState,
-        _: &Self::PaintState,
-        _: &V,
-        _: &ViewContext<V>,
-    ) -> serde_json::Value {
-        json!({
-            "type": "FacePile",
-            "bounds": bounds.to_json()
-        })
+impl RenderOnce for FacePile {
+    fn render(self, _: &mut WindowContext) -> impl IntoElement {
+        let player_count = self.faces.len();
+        let player_list = self.faces.into_iter().enumerate().map(|(ix, player)| {
+            let isnt_last = ix < player_count - 1;
+
+            div()
+                .z_index((player_count - ix) as u8)
+                .when(isnt_last, |div| div.neg_mr_1())
+                .child(player)
+        });
+        div().p_1().flex().items_center().children(player_list)
     }
 }
 
-impl<V: View> Extend<AnyElement<V>> for FacePile<V> {
-    fn extend<T: IntoIterator<Item = AnyElement<V>>>(&mut self, children: T) {
-        self.faces.extend(children);
+impl ParentElement for FacePile {
+    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
+        &mut self.faces
     }
 }

crates/collab_ui/src/notification_panel.rs 🔗

@@ -1,4 +1,4 @@
-use crate::{chat_panel::ChatPanel, render_avatar, NotificationPanelSettings};
+use crate::{chat_panel::ChatPanel, NotificationPanelSettings};
 use anyhow::Result;
 use channel::ChannelStore;
 use client::{Client, Notification, User, UserStore};
@@ -6,23 +6,23 @@ use collections::HashMap;
 use db::kvp::KEY_VALUE_STORE;
 use futures::StreamExt;
 use gpui::{
-    actions,
-    elements::*,
-    platform::{CursorStyle, MouseButton},
-    serde_json, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Task, View,
-    ViewContext, ViewHandle, WeakViewHandle, WindowContext,
+    actions, div, img, list, px, serde_json, AnyElement, AppContext, AsyncWindowContext,
+    CursorStyle, DismissEvent, Element, EventEmitter, FocusHandle, FocusableView,
+    InteractiveElement, IntoElement, ListAlignment, ListScrollEvent, ListState, Model,
+    ParentElement, Render, StatefulInteractiveElement, Styled, Task, View, ViewContext,
+    VisualContext, WeakView, WindowContext,
 };
 use notifications::{NotificationEntry, NotificationEvent, NotificationStore};
 use project::Fs;
 use rpc::proto;
 use serde::{Deserialize, Serialize};
-use settings::SettingsStore;
+use settings::{Settings, SettingsStore};
 use std::{sync::Arc, time::Duration};
-use theme::{ui, Theme};
 use time::{OffsetDateTime, UtcOffset};
+use ui::{h_stack, prelude::*, v_stack, Avatar, Button, Icon, IconButton, IconElement, Label};
 use util::{ResultExt, TryFutureExt};
 use workspace::{
-    dock::{DockPosition, Panel},
+    dock::{DockPosition, Panel, PanelEvent},
     Workspace,
 };
 
@@ -33,25 +33,25 @@ const NOTIFICATION_PANEL_KEY: &'static str = "NotificationPanel";
 
 pub struct NotificationPanel {
     client: Arc<Client>,
-    user_store: ModelHandle<UserStore>,
-    channel_store: ModelHandle<ChannelStore>,
-    notification_store: ModelHandle<NotificationStore>,
+    user_store: Model<UserStore>,
+    channel_store: Model<ChannelStore>,
+    notification_store: Model<NotificationStore>,
     fs: Arc<dyn Fs>,
-    width: Option<f32>,
+    width: Option<Pixels>,
     active: bool,
-    notification_list: ListState<Self>,
+    notification_list: ListState,
     pending_serialization: Task<Option<()>>,
     subscriptions: Vec<gpui::Subscription>,
-    workspace: WeakViewHandle<Workspace>,
+    workspace: WeakView<Workspace>,
     current_notification_toast: Option<(u64, Task<()>)>,
     local_timezone: UtcOffset,
-    has_focus: bool,
+    focus_handle: FocusHandle,
     mark_as_read_tasks: HashMap<u64, Task<Result<()>>>,
 }
 
 #[derive(Serialize, Deserialize)]
 struct SerializedNotificationPanel {
-    width: Option<f32>,
+    width: Option<Pixels>,
 }
 
 #[derive(Debug)]
@@ -71,16 +71,23 @@ pub struct NotificationPresenter {
 
 actions!(notification_panel, [ToggleFocus]);
 
-pub fn init(_cx: &mut AppContext) {}
+pub fn init(cx: &mut AppContext) {
+    cx.observe_new_views(|workspace: &mut Workspace, _| {
+        workspace.register_action(|workspace, _: &ToggleFocus, cx| {
+            workspace.toggle_panel_focus::<NotificationPanel>(cx);
+        });
+    })
+    .detach();
+}
 
 impl NotificationPanel {
-    pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
+    pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
         let fs = workspace.app_state().fs.clone();
         let client = workspace.app_state().client.clone();
         let user_store = workspace.app_state().user_store.clone();
         let workspace_handle = workspace.weak_handle();
 
-        cx.add_view(|cx| {
+        cx.new_view(|cx: &mut ViewContext<Self>| {
             let mut status = client.status();
             cx.spawn(|this, mut cx| async move {
                 while let Some(_) = status.next().await {
@@ -96,33 +103,39 @@ impl NotificationPanel {
             })
             .detach();
 
-            let mut notification_list =
-                ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| {
-                    this.render_notification(ix, cx)
-                        .unwrap_or_else(|| Empty::new().into_any())
+            let view = cx.view().downgrade();
+            let notification_list =
+                ListState::new(0, ListAlignment::Top, px(1000.), move |ix, cx| {
+                    view.upgrade()
+                        .and_then(|view| {
+                            view.update(cx, |this, cx| this.render_notification(ix, cx))
+                        })
+                        .unwrap_or_else(|| div().into_any())
                 });
-            notification_list.set_scroll_handler(|visible_range, count, this, cx| {
-                if count.saturating_sub(visible_range.end) < LOADING_THRESHOLD {
-                    if let Some(task) = this
-                        .notification_store
-                        .update(cx, |store, cx| store.load_more_notifications(false, cx))
-                    {
-                        task.detach();
+            notification_list.set_scroll_handler(cx.listener(
+                |this, event: &ListScrollEvent, cx| {
+                    if event.count.saturating_sub(event.visible_range.end) < LOADING_THRESHOLD {
+                        if let Some(task) = this
+                            .notification_store
+                            .update(cx, |store, cx| store.load_more_notifications(false, cx))
+                        {
+                            task.detach();
+                        }
                     }
-                }
-            });
+                },
+            ));
 
             let mut this = Self {
                 fs,
                 client,
                 user_store,
-                local_timezone: cx.platform().local_timezone(),
+                local_timezone: cx.local_timezone(),
                 channel_store: ChannelStore::global(cx),
                 notification_store: NotificationStore::global(cx),
                 notification_list,
                 pending_serialization: Task::ready(None),
                 workspace: workspace_handle,
-                has_focus: false,
+                focus_handle: cx.focus_handle(),
                 current_notification_toast: None,
                 subscriptions: Vec::new(),
                 active: false,
@@ -134,7 +147,7 @@ impl NotificationPanel {
             this.subscriptions.extend([
                 cx.observe(&this.notification_store, |_, _, cx| cx.notify()),
                 cx.subscribe(&this.notification_store, Self::on_notification_event),
-                cx.observe_global::<SettingsStore, _>(move |this: &mut Self, cx| {
+                cx.observe_global::<SettingsStore>(move |this: &mut Self, cx| {
                     let new_dock_position = this.position(cx);
                     if new_dock_position != old_dock_position {
                         old_dock_position = new_dock_position;
@@ -148,12 +161,12 @@ impl NotificationPanel {
     }
 
     pub fn load(
-        workspace: WeakViewHandle<Workspace>,
-        cx: AsyncAppContext,
-    ) -> Task<Result<ViewHandle<Self>>> {
+        workspace: WeakView<Workspace>,
+        cx: AsyncWindowContext,
+    ) -> Task<Result<View<Self>>> {
         cx.spawn(|mut cx| async move {
             let serialized_panel = if let Some(panel) = cx
-                .background()
+                .background_executor()
                 .spawn(async move { KEY_VALUE_STORE.read_kvp(NOTIFICATION_PANEL_KEY) })
                 .await
                 .log_err()
@@ -179,7 +192,7 @@ impl NotificationPanel {
 
     fn serialize(&mut self, cx: &mut ViewContext<Self>) {
         let width = self.width;
-        self.pending_serialization = cx.background().spawn(
+        self.pending_serialization = cx.background_executor().spawn(
             async move {
                 KEY_VALUE_STORE
                     .write_kvp(
@@ -193,11 +206,7 @@ impl NotificationPanel {
         );
     }
 
-    fn render_notification(
-        &mut self,
-        ix: usize,
-        cx: &mut ViewContext<Self>,
-    ) -> Option<AnyElement<Self>> {
+    fn render_notification(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
         let entry = self.notification_store.read(cx).notification_at(ix)?;
         let notification_id = entry.id;
         let now = OffsetDateTime::now_utc();
@@ -210,136 +219,99 @@ impl NotificationPanel {
             ..
         } = self.present_notification(entry, cx)?;
 
-        let theme = theme::current(cx);
-        let style = &theme.notification_panel;
         let response = entry.response;
         let notification = entry.notification.clone();
 
-        let message_style = if entry.is_read {
-            style.read_text.clone()
-        } else {
-            style.unread_text.clone()
-        };
-
         if self.active && !entry.is_read {
             self.did_render_notification(notification_id, &notification, cx);
         }
 
-        enum Decline {}
-        enum Accept {}
-
         Some(
-            MouseEventHandler::new::<NotificationEntry, _>(ix, cx, |_, cx| {
-                let container = message_style.container;
-
-                Flex::row()
-                    .with_children(actor.map(|actor| {
-                        render_avatar(actor.avatar.clone(), &style.avatar, style.avatar_container)
-                    }))
-                    .with_child(
-                        Flex::column()
-                            .with_child(Text::new(text, message_style.text.clone()))
-                            .with_child(
-                                Flex::row()
-                                    .with_child(
-                                        Label::new(
-                                            format_timestamp(timestamp, now, self.local_timezone),
-                                            style.timestamp.text.clone(),
-                                        )
-                                        .contained()
-                                        .with_style(style.timestamp.container),
+            div()
+                .id(ix)
+                .flex()
+                .flex_row()
+                .size_full()
+                .px_2()
+                .py_1()
+                .gap_2()
+                .when(can_navigate, |el| {
+                    el.cursor(CursorStyle::PointingHand).on_click({
+                        let notification = notification.clone();
+                        cx.listener(move |this, _, cx| {
+                            this.did_click_notification(&notification, cx)
+                        })
+                    })
+                })
+                .children(actor.map(|actor| {
+                    img(actor.avatar_uri.clone())
+                        .flex_none()
+                        .w_8()
+                        .h_8()
+                        .rounded_full()
+                }))
+                .child(
+                    v_stack()
+                        .gap_1()
+                        .size_full()
+                        .overflow_hidden()
+                        .child(Label::new(text.clone()))
+                        .child(
+                            h_stack()
+                                .child(
+                                    Label::new(format_timestamp(
+                                        timestamp,
+                                        now,
+                                        self.local_timezone,
+                                    ))
+                                    .color(Color::Muted),
+                                )
+                                .children(if let Some(is_accepted) = response {
+                                    Some(div().flex().flex_grow().justify_end().child(Label::new(
+                                        if is_accepted {
+                                            "You accepted"
+                                        } else {
+                                            "You declined"
+                                        },
+                                    )))
+                                } else if needs_response {
+                                    Some(
+                                        h_stack()
+                                            .flex_grow()
+                                            .justify_end()
+                                            .child(Button::new("decline", "Decline").on_click({
+                                                let notification = notification.clone();
+                                                let view = cx.view().clone();
+                                                move |_, cx| {
+                                                    view.update(cx, |this, cx| {
+                                                        this.respond_to_notification(
+                                                            notification.clone(),
+                                                            false,
+                                                            cx,
+                                                        )
+                                                    });
+                                                }
+                                            }))
+                                            .child(Button::new("accept", "Accept").on_click({
+                                                let notification = notification.clone();
+                                                let view = cx.view().clone();
+                                                move |_, cx| {
+                                                    view.update(cx, |this, cx| {
+                                                        this.respond_to_notification(
+                                                            notification.clone(),
+                                                            true,
+                                                            cx,
+                                                        )
+                                                    });
+                                                }
+                                            })),
                                     )
-                                    .with_children(if let Some(is_accepted) = response {
-                                        Some(
-                                            Label::new(
-                                                if is_accepted {
-                                                    "You accepted"
-                                                } else {
-                                                    "You declined"
-                                                },
-                                                style.read_text.text.clone(),
-                                            )
-                                            .flex_float()
-                                            .into_any(),
-                                        )
-                                    } else if needs_response {
-                                        Some(
-                                            Flex::row()
-                                                .with_children([
-                                                    MouseEventHandler::new::<Decline, _>(
-                                                        ix,
-                                                        cx,
-                                                        |state, _| {
-                                                            let button =
-                                                                style.button.style_for(state);
-                                                            Label::new(
-                                                                "Decline",
-                                                                button.text.clone(),
-                                                            )
-                                                            .contained()
-                                                            .with_style(button.container)
-                                                        },
-                                                    )
-                                                    .with_cursor_style(CursorStyle::PointingHand)
-                                                    .on_click(MouseButton::Left, {
-                                                        let notification = notification.clone();
-                                                        move |_, view, cx| {
-                                                            view.respond_to_notification(
-                                                                notification.clone(),
-                                                                false,
-                                                                cx,
-                                                            );
-                                                        }
-                                                    }),
-                                                    MouseEventHandler::new::<Accept, _>(
-                                                        ix,
-                                                        cx,
-                                                        |state, _| {
-                                                            let button =
-                                                                style.button.style_for(state);
-                                                            Label::new(
-                                                                "Accept",
-                                                                button.text.clone(),
-                                                            )
-                                                            .contained()
-                                                            .with_style(button.container)
-                                                        },
-                                                    )
-                                                    .with_cursor_style(CursorStyle::PointingHand)
-                                                    .on_click(MouseButton::Left, {
-                                                        let notification = notification.clone();
-                                                        move |_, view, cx| {
-                                                            view.respond_to_notification(
-                                                                notification.clone(),
-                                                                true,
-                                                                cx,
-                                                            );
-                                                        }
-                                                    }),
-                                                ])
-                                                .flex_float()
-                                                .into_any(),
-                                        )
-                                    } else {
-                                        None
-                                    }),
-                            )
-                            .flex(1.0, true),
-                    )
-                    .contained()
-                    .with_style(container)
-                    .into_any()
-            })
-            .with_cursor_style(if can_navigate {
-                CursorStyle::PointingHand
-            } else {
-                CursorStyle::default()
-            })
-            .on_click(MouseButton::Left, {
-                let notification = notification.clone();
-                move |_, this, cx| this.did_click_notification(&notification, cx)
-            })
-            .into_any(),
+                                } else {
+                                    None
+                                }),
+                        ),
+                )
+                .into_any(),
         )
     }
 
@@ -432,7 +404,7 @@ impl NotificationPanel {
                 .or_insert_with(|| {
                     let client = self.client.clone();
                     cx.spawn(|this, mut cx| async move {
-                        cx.background().timer(MARK_AS_READ_DELAY).await;
+                        cx.background_executor().timer(MARK_AS_READ_DELAY).await;
                         client
                             .request(proto::MarkNotificationRead { notification_id })
                             .await?;
@@ -452,8 +424,8 @@ impl NotificationPanel {
             ..
         } = notification.clone()
         {
-            if let Some(workspace) = self.workspace.upgrade(cx) {
-                cx.app_context().defer(move |cx| {
+            if let Some(workspace) = self.workspace.upgrade() {
+                cx.window_context().defer(move |cx| {
                     workspace.update(cx, |workspace, cx| {
                         if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
                             panel.update(cx, |panel, cx| {
@@ -468,73 +440,27 @@ impl NotificationPanel {
         }
     }
 
-    fn is_showing_notification(&self, notification: &Notification, cx: &AppContext) -> bool {
+    fn is_showing_notification(&self, notification: &Notification, cx: &ViewContext<Self>) -> bool {
         if let Notification::ChannelMessageMention { channel_id, .. } = &notification {
-            if let Some(workspace) = self.workspace.upgrade(cx) {
-                return workspace
-                    .read_with(cx, |workspace, cx| {
-                        if let Some(panel) = workspace.panel::<ChatPanel>(cx) {
-                            return panel.read_with(cx, |panel, cx| {
-                                panel.is_scrolled_to_bottom()
-                                    && panel.active_chat().map_or(false, |chat| {
-                                        chat.read(cx).channel_id == *channel_id
-                                    })
-                            });
-                        }
-                        false
-                    })
-                    .unwrap_or_default();
+            if let Some(workspace) = self.workspace.upgrade() {
+                return if let Some(panel) = workspace.read(cx).panel::<ChatPanel>(cx) {
+                    let panel = panel.read(cx);
+                    panel.is_scrolled_to_bottom()
+                        && panel
+                            .active_chat()
+                            .map_or(false, |chat| chat.read(cx).channel_id == *channel_id)
+                } else {
+                    false
+                };
             }
         }
 
         false
     }
 
-    fn render_sign_in_prompt(
-        &self,
-        theme: &Arc<Theme>,
-        cx: &mut ViewContext<Self>,
-    ) -> AnyElement<Self> {
-        enum SignInPromptLabel {}
-
-        MouseEventHandler::new::<SignInPromptLabel, _>(0, cx, |mouse_state, _| {
-            Label::new(
-                "Sign in to view your notifications".to_string(),
-                theme
-                    .chat_panel
-                    .sign_in_prompt
-                    .style_for(mouse_state)
-                    .clone(),
-            )
-        })
-        .with_cursor_style(CursorStyle::PointingHand)
-        .on_click(MouseButton::Left, move |_, this, cx| {
-            let client = this.client.clone();
-            cx.spawn(|_, cx| async move {
-                client.authenticate_and_connect(true, &cx).log_err().await;
-            })
-            .detach();
-        })
-        .aligned()
-        .into_any()
-    }
-
-    fn render_empty_state(
-        &self,
-        theme: &Arc<Theme>,
-        _cx: &mut ViewContext<Self>,
-    ) -> AnyElement<Self> {
-        Label::new(
-            "You have no notifications".to_string(),
-            theme.chat_panel.sign_in_prompt.default.clone(),
-        )
-        .aligned()
-        .into_any()
-    }
-
     fn on_notification_event(
         &mut self,
-        _: ModelHandle<NotificationStore>,
+        _: Model<NotificationStore>,
         event: &NotificationEvent,
         cx: &mut ViewContext<Self>,
     ) {
@@ -566,7 +492,7 @@ impl NotificationPanel {
         self.current_notification_toast = Some((
             notification_id,
             cx.spawn(|this, mut cx| async move {
-                cx.background().timer(TOAST_DURATION).await;
+                cx.background_executor().timer(TOAST_DURATION).await;
                 this.update(&mut cx, |this, cx| this.remove_toast(notification_id, cx))
                     .ok();
             }),
@@ -576,8 +502,8 @@ impl NotificationPanel {
             .update(cx, |workspace, cx| {
                 workspace.dismiss_notification::<NotificationToast>(0, cx);
                 workspace.show_notification(0, cx, |cx| {
-                    let workspace = cx.weak_handle();
-                    cx.add_view(|_| NotificationToast {
+                    let workspace = cx.view().downgrade();
+                    cx.new_view(|_| NotificationToast {
                         notification_id,
                         actor,
                         text,
@@ -613,62 +539,90 @@ impl NotificationPanel {
     }
 }
 
-impl Entity for NotificationPanel {
-    type Event = Event;
-}
-
-impl View for NotificationPanel {
-    fn ui_name() -> &'static str {
-        "NotificationPanel"
+impl Render for NotificationPanel {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+        v_stack()
+            .size_full()
+            .child(
+                h_stack()
+                    .justify_between()
+                    .px_2()
+                    .py_1()
+                    // Match the height of the tab bar so they line up.
+                    .h(rems(ui::Tab::HEIGHT_IN_REMS))
+                    .border_b_1()
+                    .border_color(cx.theme().colors().border)
+                    .child(Label::new("Notifications"))
+                    .child(IconElement::new(Icon::Envelope)),
+            )
+            .map(|this| {
+                if self.client.user_id().is_none() {
+                    this.child(
+                        v_stack()
+                            .gap_2()
+                            .p_4()
+                            .child(
+                                Button::new("sign_in_prompt_button", "Sign in")
+                                    .icon_color(Color::Muted)
+                                    .icon(Icon::Github)
+                                    .icon_position(IconPosition::Start)
+                                    .style(ButtonStyle::Filled)
+                                    .full_width()
+                                    .on_click({
+                                        let client = self.client.clone();
+                                        move |_, cx| {
+                                            let client = client.clone();
+                                            cx.spawn(move |cx| async move {
+                                                client
+                                                    .authenticate_and_connect(true, &cx)
+                                                    .log_err()
+                                                    .await;
+                                            })
+                                            .detach()
+                                        }
+                                    }),
+                            )
+                            .child(
+                                div().flex().w_full().items_center().child(
+                                    Label::new("Sign in to view notifications.")
+                                        .color(Color::Muted)
+                                        .size(LabelSize::Small),
+                                ),
+                            ),
+                    )
+                } else if self.notification_list.item_count() == 0 {
+                    this.child(
+                        v_stack().p_4().child(
+                            div().flex().w_full().items_center().child(
+                                Label::new("You have no notifications.")
+                                    .color(Color::Muted)
+                                    .size(LabelSize::Small),
+                            ),
+                        ),
+                    )
+                } else {
+                    this.child(list(self.notification_list.clone()).size_full())
+                }
+            })
     }
+}
 
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let theme = theme::current(cx);
-        let style = &theme.notification_panel;
-        let element = if self.client.user_id().is_none() {
-            self.render_sign_in_prompt(&theme, cx)
-        } else if self.notification_list.item_count() == 0 {
-            self.render_empty_state(&theme, cx)
-        } else {
-            Flex::column()
-                .with_child(
-                    Flex::row()
-                        .with_child(Label::new("Notifications", style.title.text.clone()))
-                        .with_child(ui::svg(&style.title_icon).flex_float())
-                        .align_children_center()
-                        .contained()
-                        .with_style(style.title.container)
-                        .constrained()
-                        .with_height(style.title_height),
-                )
-                .with_child(
-                    List::new(self.notification_list.clone())
-                        .contained()
-                        .with_style(style.list)
-                        .flex(1., true),
-                )
-                .into_any()
-        };
-        element
-            .contained()
-            .with_style(style.container)
-            .constrained()
-            .with_min_width(150.)
-            .into_any()
+impl FocusableView for NotificationPanel {
+    fn focus_handle(&self, _: &AppContext) -> FocusHandle {
+        self.focus_handle.clone()
     }
+}
 
-    fn focus_in(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
-        self.has_focus = true;
-    }
+impl EventEmitter<Event> for NotificationPanel {}
+impl EventEmitter<PanelEvent> for NotificationPanel {}
 
-    fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
-        self.has_focus = false;
+impl Panel for NotificationPanel {
+    fn persistent_name() -> &'static str {
+        "NotificationPanel"
     }
-}
 
-impl Panel for NotificationPanel {
     fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
-        settings::get::<NotificationPanelSettings>(cx).dock
+        NotificationPanelSettings::get_global(cx).dock
     }
 
     fn position_is_valid(&self, position: DockPosition) -> bool {
@@ -683,12 +637,12 @@ impl Panel for NotificationPanel {
         );
     }
 
-    fn size(&self, cx: &gpui::WindowContext) -> f32 {
+    fn size(&self, cx: &gpui::WindowContext) -> Pixels {
         self.width
-            .unwrap_or_else(|| settings::get::<NotificationPanelSettings>(cx).default_width)
+            .unwrap_or_else(|| NotificationPanelSettings::get_global(cx).default_width)
     }
 
-    fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
+    fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
         self.width = size;
         self.serialize(cx);
         cx.notify();
@@ -701,17 +655,14 @@ impl Panel for NotificationPanel {
         }
     }
 
-    fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
-        (settings::get::<NotificationPanelSettings>(cx).button
+    fn icon(&self, cx: &gpui::WindowContext) -> Option<Icon> {
+        (NotificationPanelSettings::get_global(cx).button
             && self.notification_store.read(cx).notification_count() > 0)
-            .then(|| "icons/bell.svg")
+            .then(|| Icon::Bell)
     }
 
-    fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
-        (
-            "Notification Panel".to_string(),
-            Some(Box::new(ToggleFocus)),
-        )
+    fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
+        Some("Notification Panel")
     }
 
     fn icon_label(&self, cx: &WindowContext) -> Option<String> {
@@ -723,20 +674,8 @@ impl Panel for NotificationPanel {
         }
     }
 
-    fn should_change_position_on_event(event: &Self::Event) -> bool {
-        matches!(event, Event::DockPositionChanged)
-    }
-
-    fn should_close_on_event(event: &Self::Event) -> bool {
-        matches!(event, Event::Dismissed)
-    }
-
-    fn has_focus(&self, _cx: &gpui::WindowContext) -> bool {
-        self.has_focus
-    }
-
-    fn is_focus_event(event: &Self::Event) -> bool {
-        matches!(event, Event::Focus)
+    fn toggle_action(&self) -> Box<dyn gpui::Action> {
+        Box::new(ToggleFocus)
     }
 }
 
@@ -744,18 +683,14 @@ pub struct NotificationToast {
     notification_id: u64,
     actor: Option<Arc<User>>,
     text: String,
-    workspace: WeakViewHandle<Workspace>,
-}
-
-pub enum ToastEvent {
-    Dismiss,
+    workspace: WeakView<Workspace>,
 }
 
 impl NotificationToast {
-    fn focus_notification_panel(&self, cx: &mut AppContext) {
+    fn focus_notification_panel(&self, cx: &mut ViewContext<Self>) {
         let workspace = self.workspace.clone();
         let notification_id = self.notification_id;
-        cx.defer(move |cx| {
+        cx.window_context().defer(move |cx| {
             workspace
                 .update(cx, |workspace, cx| {
                     if let Some(panel) = workspace.focus_panel::<NotificationPanel>(cx) {
@@ -772,91 +707,27 @@ impl NotificationToast {
     }
 }
 
-impl Entity for NotificationToast {
-    type Event = ToastEvent;
-}
-
-impl View for NotificationToast {
-    fn ui_name() -> &'static str {
-        "ContactNotification"
-    }
-
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+impl Render for NotificationToast {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
         let user = self.actor.clone();
-        let theme = theme::current(cx).clone();
-        let theme = &theme.contact_notification;
-
-        MouseEventHandler::new::<Self, _>(0, cx, |_, cx| {
-            Flex::row()
-                .with_children(user.and_then(|user| {
-                    Some(
-                        Image::from_data(user.avatar.clone()?)
-                            .with_style(theme.header_avatar)
-                            .aligned()
-                            .constrained()
-                            .with_height(
-                                cx.font_cache()
-                                    .line_height(theme.header_message.text.font_size),
-                            )
-                            .aligned()
-                            .top(),
-                    )
-                }))
-                .with_child(
-                    Text::new(self.text.clone(), theme.header_message.text.clone())
-                        .contained()
-                        .with_style(theme.header_message.container)
-                        .aligned()
-                        .top()
-                        .left()
-                        .flex(1., true),
-                )
-                .with_child(
-                    MouseEventHandler::new::<ToastEvent, _>(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_cursor_style(CursorStyle::PointingHand)
-                    .with_padding(Padding::uniform(5.))
-                    .on_click(MouseButton::Left, move |_, _, cx| {
-                        cx.emit(ToastEvent::Dismiss)
-                    })
-                    .aligned()
-                    .constrained()
-                    .with_height(
-                        cx.font_cache()
-                            .line_height(theme.header_message.text.font_size),
-                    )
-                    .aligned()
-                    .top()
-                    .flex_float(),
-                )
-                .contained()
-        })
-        .with_cursor_style(CursorStyle::PointingHand)
-        .on_click(MouseButton::Left, move |_, this, cx| {
-            this.focus_notification_panel(cx);
-            cx.emit(ToastEvent::Dismiss);
-        })
-        .into_any()
-    }
-}
 
-impl workspace::notifications::Notification for NotificationToast {
-    fn should_dismiss_notification_on_event(&self, event: &<Self as Entity>::Event) -> bool {
-        matches!(event, ToastEvent::Dismiss)
+        h_stack()
+            .id("notification_panel_toast")
+            .children(user.map(|user| Avatar::new(user.avatar_uri.clone())))
+            .child(Label::new(self.text.clone()))
+            .child(
+                IconButton::new("close", Icon::Close)
+                    .on_click(cx.listener(|_, _, cx| cx.emit(DismissEvent))),
+            )
+            .on_click(cx.listener(|this, _, cx| {
+                this.focus_notification_panel(cx);
+                cx.emit(DismissEvent);
+            }))
     }
 }
 
+impl EventEmitter<DismissEvent> for NotificationToast {}
+
 fn format_timestamp(
     mut timestamp: OffsetDateTime,
     mut now: OffsetDateTime,

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

@@ -1,14 +1,15 @@
 use crate::notification_window_options;
 use call::{ActiveCall, IncomingCall};
-use client::proto;
 use futures::StreamExt;
 use gpui::{
-    elements::*,
-    geometry::vector::vec2f,
-    platform::{CursorStyle, MouseButton},
-    AnyElement, AppContext, Entity, View, ViewContext, WindowHandle,
+    img, px, AppContext, ParentElement, Render, RenderOnce, Styled, ViewContext,
+    VisualContext as _, WindowHandle,
 };
+use settings::Settings;
 use std::sync::{Arc, Weak};
+use theme::ThemeSettings;
+use ui::prelude::*;
+use ui::{h_stack, v_stack, Button, Label};
 use util::ResultExt;
 use workspace::AppState;
 
@@ -19,21 +20,33 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
         let mut notification_windows: Vec<WindowHandle<IncomingCallNotification>> = Vec::new();
         while let Some(incoming_call) = incoming_call.next().await {
             for window in notification_windows.drain(..) {
-                window.remove(&mut cx);
+                window
+                    .update(&mut cx, |_, cx| {
+                        // todo!()
+                        cx.remove_window();
+                    })
+                    .log_err();
             }
 
             if let Some(incoming_call) = incoming_call {
-                let window_size = cx.read(|cx| {
-                    let theme = &theme::current(cx).incoming_call_notification;
-                    vec2f(theme.window_width, theme.window_height)
-                });
+                let unique_screens = cx.update(|cx| cx.displays()).unwrap();
+                let window_size = gpui::Size {
+                    width: px(380.),
+                    height: px(64.),
+                };
 
-                for screen in cx.platform().screens() {
+                for screen in unique_screens {
+                    let options = notification_window_options(screen, window_size);
                     let window = cx
-                        .add_window(notification_window_options(screen, window_size), |_| {
-                            IncomingCallNotification::new(incoming_call.clone(), app_state.clone())
-                        });
-
+                        .open_window(options, |cx| {
+                            cx.new_view(|_| {
+                                IncomingCallNotification::new(
+                                    incoming_call.clone(),
+                                    app_state.clone(),
+                                )
+                            })
+                        })
+                        .unwrap();
                     notification_windows.push(window);
                 }
             }
@@ -47,167 +60,104 @@ struct RespondToCall {
     accept: bool,
 }
 
-pub struct IncomingCallNotification {
+struct IncomingCallNotificationState {
     call: IncomingCall,
     app_state: Weak<AppState>,
 }
 
-impl IncomingCallNotification {
+pub struct IncomingCallNotification {
+    state: Arc<IncomingCallNotificationState>,
+}
+impl IncomingCallNotificationState {
     pub fn new(call: IncomingCall, app_state: Weak<AppState>) -> Self {
         Self { call, app_state }
     }
 
-    fn respond(&mut self, accept: bool, cx: &mut ViewContext<Self>) {
+    fn respond(&self, accept: bool, cx: &mut AppContext) {
         let active_call = ActiveCall::global(cx);
         if accept {
             let join = active_call.update(cx, |active_call, cx| active_call.accept_incoming(cx));
             let caller_user_id = self.call.calling_user.id;
             let initial_project_id = self.call.initial_project.as_ref().map(|project| project.id);
             let app_state = self.app_state.clone();
-            cx.app_context()
-                .spawn(|mut cx| async move {
-                    join.await?;
-                    if let Some(project_id) = initial_project_id {
-                        cx.update(|cx| {
-                            if let Some(app_state) = app_state.upgrade() {
-                                workspace::join_remote_project(
-                                    project_id,
-                                    caller_user_id,
-                                    app_state,
-                                    cx,
-                                )
-                                .detach_and_log_err(cx);
-                            }
-                        });
-                    }
-                    anyhow::Ok(())
-                })
-                .detach_and_log_err(cx);
+            let cx: &mut AppContext = cx;
+            cx.spawn(|cx| async move {
+                join.await?;
+                if let Some(project_id) = initial_project_id {
+                    cx.update(|cx| {
+                        if let Some(app_state) = app_state.upgrade() {
+                            workspace::join_remote_project(
+                                project_id,
+                                caller_user_id,
+                                app_state,
+                                cx,
+                            )
+                            .detach_and_log_err(cx);
+                        }
+                    })
+                    .log_err();
+                }
+                anyhow::Ok(())
+            })
+            .detach_and_log_err(cx);
         } else {
             active_call.update(cx, |active_call, cx| {
                 active_call.decline_incoming(cx).log_err();
             });
         }
     }
+}
 
-    fn render_caller(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let theme = &theme::current(cx).incoming_call_notification;
-        let default_project = proto::ParticipantProject::default();
-        let initial_project = self
-            .call
-            .initial_project
-            .as_ref()
-            .unwrap_or(&default_project);
-        Flex::row()
-            .with_children(self.call.calling_user.avatar.clone().map(|avatar| {
-                Image::from_data(avatar)
-                    .with_style(theme.caller_avatar)
-                    .aligned()
-            }))
-            .with_child(
-                Flex::column()
-                    .with_child(
-                        Label::new(
-                            self.call.calling_user.github_login.clone(),
-                            theme.caller_username.text.clone(),
-                        )
-                        .contained()
-                        .with_style(theme.caller_username.container),
-                    )
-                    .with_child(
-                        Label::new(
-                            format!(
-                                "is sharing a project in Zed{}",
-                                if initial_project.worktree_root_names.is_empty() {
-                                    ""
-                                } else {
-                                    ":"
-                                }
-                            ),
-                            theme.caller_message.text.clone(),
-                        )
-                        .contained()
-                        .with_style(theme.caller_message.container),
-                    )
-                    .with_children(if initial_project.worktree_root_names.is_empty() {
-                        None
-                    } else {
-                        Some(
-                            Label::new(
-                                initial_project.worktree_root_names.join(", "),
-                                theme.worktree_roots.text.clone(),
-                            )
-                            .contained()
-                            .with_style(theme.worktree_roots.container),
-                        )
-                    })
-                    .contained()
-                    .with_style(theme.caller_metadata)
-                    .aligned(),
-            )
-            .contained()
-            .with_style(theme.caller_container)
-            .flex(1., true)
-            .into_any()
-    }
-
-    fn render_buttons(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        enum Accept {}
-        enum Decline {}
-
-        let theme = theme::current(cx);
-        Flex::column()
-            .with_child(
-                MouseEventHandler::new::<Accept, _>(0, cx, |_, _| {
-                    let theme = &theme.incoming_call_notification;
-                    Label::new("Accept", theme.accept_button.text.clone())
-                        .aligned()
-                        .contained()
-                        .with_style(theme.accept_button.container)
-                })
-                .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(MouseButton::Left, |_, this, cx| {
-                    this.respond(true, cx);
-                })
-                .flex(1., true),
-            )
-            .with_child(
-                MouseEventHandler::new::<Decline, _>(0, cx, |_, _| {
-                    let theme = &theme.incoming_call_notification;
-                    Label::new("Decline", theme.decline_button.text.clone())
-                        .aligned()
-                        .contained()
-                        .with_style(theme.decline_button.container)
-                })
-                .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(MouseButton::Left, |_, this, cx| {
-                    this.respond(false, cx);
-                })
-                .flex(1., true),
-            )
-            .constrained()
-            .with_width(theme.incoming_call_notification.button_width)
-            .into_any()
+impl IncomingCallNotification {
+    pub fn new(call: IncomingCall, app_state: Weak<AppState>) -> Self {
+        Self {
+            state: Arc::new(IncomingCallNotificationState::new(call, app_state)),
+        }
     }
 }
 
-impl Entity for IncomingCallNotification {
-    type Event = ();
-}
+impl Render for IncomingCallNotification {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+        // TODO: Is there a better place for us to initialize the font?
+        let (ui_font, ui_font_size) = {
+            let theme_settings = ThemeSettings::get_global(cx);
+            (
+                theme_settings.ui_font.family.clone(),
+                theme_settings.ui_font_size.clone(),
+            )
+        };
 
-impl View for IncomingCallNotification {
-    fn ui_name() -> &'static str {
-        "IncomingCallNotification"
-    }
+        cx.set_rem_size(ui_font_size);
 
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let background = theme::current(cx).incoming_call_notification.background;
-        Flex::row()
-            .with_child(self.render_caller(cx))
-            .with_child(self.render_buttons(cx))
-            .contained()
-            .with_background_color(background)
-            .expanded()
-            .into_any()
+        h_stack()
+            .font(ui_font)
+            .text_ui()
+            .justify_between()
+            .size_full()
+            .overflow_hidden()
+            .elevation_3(cx)
+            .p_2()
+            .gap_2()
+            .child(
+                img(self.state.call.calling_user.avatar_uri.clone())
+                    .w_12()
+                    .h_12()
+                    .rounded_full(),
+            )
+            .child(v_stack().overflow_hidden().child(Label::new(format!(
+                "{} is sharing a project in Zed",
+                self.state.call.calling_user.github_login
+            ))))
+            .child(
+                v_stack()
+                    .child(Button::new("accept", "Accept").render(cx).on_click({
+                        let state = self.state.clone();
+                        move |_, cx| state.respond(true, cx)
+                    }))
+                    .child(Button::new("decline", "Decline").render(cx).on_click({
+                        let state = self.state.clone();
+                        move |_, cx| state.respond(false, cx)
+                    })),
+            )
     }
 }

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

@@ -2,13 +2,11 @@ use crate::notification_window_options;
 use call::{room, ActiveCall};
 use client::User;
 use collections::HashMap;
-use gpui::{
-    elements::*,
-    geometry::vector::vec2f,
-    platform::{CursorStyle, MouseButton},
-    AppContext, Entity, View, ViewContext,
-};
+use gpui::{img, px, AppContext, ParentElement, Render, Size, Styled, ViewContext, VisualContext};
+use settings::Settings;
 use std::sync::{Arc, Weak};
+use theme::ThemeSettings;
+use ui::{h_stack, prelude::*, v_stack, Button, Label};
 use workspace::AppState;
 
 pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
@@ -21,38 +19,54 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
             project_id,
             worktree_root_names,
         } => {
-            let theme = &theme::current(cx).project_shared_notification;
-            let window_size = vec2f(theme.window_width, theme.window_height);
+            let window_size = Size {
+                width: px(400.),
+                height: px(72.),
+            };
 
-            for screen in cx.platform().screens() {
-                let window =
-                    cx.add_window(notification_window_options(screen, window_size), |_| {
+            for screen in cx.displays() {
+                let options = notification_window_options(screen, window_size);
+                let window = cx.open_window(options, |cx| {
+                    cx.new_view(|_| {
                         ProjectSharedNotification::new(
                             owner.clone(),
                             *project_id,
                             worktree_root_names.clone(),
                             app_state.clone(),
                         )
-                    });
+                    })
+                });
                 notification_windows
                     .entry(*project_id)
                     .or_insert(Vec::new())
                     .push(window);
             }
         }
+
         room::Event::RemoteProjectUnshared { project_id }
         | room::Event::RemoteProjectJoined { project_id }
         | room::Event::RemoteProjectInvitationDiscarded { project_id } => {
             if let Some(windows) = notification_windows.remove(&project_id) {
                 for window in windows {
-                    window.remove(cx);
+                    window
+                        .update(cx, |_, cx| {
+                            // todo!()
+                            cx.remove_window();
+                        })
+                        .ok();
                 }
             }
         }
+
         room::Event::Left => {
             for (_, windows) in notification_windows.drain() {
                 for window in windows {
-                    window.remove(cx);
+                    window
+                        .update(cx, |_, cx| {
+                            // todo!()
+                            cx.remove_window();
+                        })
+                        .ok();
                 }
             }
         }
@@ -101,117 +115,66 @@ impl ProjectSharedNotification {
             });
         }
     }
+}
 
-    fn render_owner(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let theme = &theme::current(cx).project_shared_notification;
-        Flex::row()
-            .with_children(self.owner.avatar.clone().map(|avatar| {
-                Image::from_data(avatar)
-                    .with_style(theme.owner_avatar)
-                    .aligned()
-            }))
-            .with_child(
-                Flex::column()
-                    .with_child(
-                        Label::new(
-                            self.owner.github_login.clone(),
-                            theme.owner_username.text.clone(),
-                        )
-                        .contained()
-                        .with_style(theme.owner_username.container),
-                    )
-                    .with_child(
-                        Label::new(
-                            format!(
-                                "is sharing a project in Zed{}",
-                                if self.worktree_root_names.is_empty() {
-                                    ""
-                                } else {
-                                    ":"
-                                }
-                            ),
-                            theme.message.text.clone(),
-                        )
-                        .contained()
-                        .with_style(theme.message.container),
-                    )
-                    .with_children(if self.worktree_root_names.is_empty() {
-                        None
-                    } else {
-                        Some(
-                            Label::new(
-                                self.worktree_root_names.join(", "),
-                                theme.worktree_roots.text.clone(),
-                            )
-                            .contained()
-                            .with_style(theme.worktree_roots.container),
-                        )
-                    })
-                    .contained()
-                    .with_style(theme.owner_metadata)
-                    .aligned(),
+impl Render for ProjectSharedNotification {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+        // TODO: Is there a better place for us to initialize the font?
+        let (ui_font, ui_font_size) = {
+            let theme_settings = ThemeSettings::get_global(cx);
+            (
+                theme_settings.ui_font.family.clone(),
+                theme_settings.ui_font_size.clone(),
             )
-            .contained()
-            .with_style(theme.owner_container)
-            .flex(1., true)
-            .into_any()
-    }
+        };
 
-    fn render_buttons(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        enum Open {}
-        enum Dismiss {}
+        cx.set_rem_size(ui_font_size);
 
-        let theme = theme::current(cx);
-        Flex::column()
-            .with_child(
-                MouseEventHandler::new::<Open, _>(0, cx, |_, _| {
-                    let theme = &theme.project_shared_notification;
-                    Label::new("Open", theme.open_button.text.clone())
-                        .aligned()
-                        .contained()
-                        .with_style(theme.open_button.container)
-                })
-                .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(MouseButton::Left, move |_, this, cx| this.join(cx))
-                .flex(1., true),
+        h_stack()
+            .font(ui_font)
+            .text_ui()
+            .justify_between()
+            .size_full()
+            .overflow_hidden()
+            .elevation_3(cx)
+            .p_2()
+            .gap_2()
+            .child(
+                img(self.owner.avatar_uri.clone())
+                    .w_12()
+                    .h_12()
+                    .rounded_full(),
             )
-            .with_child(
-                MouseEventHandler::new::<Dismiss, _>(0, cx, |_, _| {
-                    let theme = &theme.project_shared_notification;
-                    Label::new("Dismiss", theme.dismiss_button.text.clone())
-                        .aligned()
-                        .contained()
-                        .with_style(theme.dismiss_button.container)
-                })
-                .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(MouseButton::Left, |_, this, cx| {
-                    this.dismiss(cx);
-                })
-                .flex(1., true),
+            .child(
+                v_stack()
+                    .overflow_hidden()
+                    .child(Label::new(self.owner.github_login.clone()))
+                    .child(Label::new(format!(
+                        "is sharing a project in Zed{}",
+                        if self.worktree_root_names.is_empty() {
+                            ""
+                        } else {
+                            ":"
+                        }
+                    )))
+                    .children(if self.worktree_root_names.is_empty() {
+                        None
+                    } else {
+                        Some(Label::new(self.worktree_root_names.join(", ")))
+                    }),
+            )
+            .child(
+                v_stack()
+                    .child(Button::new("open", "Open").on_click(cx.listener(
+                        move |this, _event, cx| {
+                            this.join(cx);
+                        },
+                    )))
+                    .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
+                        move |this, _event, cx| {
+                            this.dismiss(cx);
+                        },
+                    ))),
             )
-            .constrained()
-            .with_width(theme.project_shared_notification.button_width)
-            .into_any()
-    }
-}
-
-impl Entity for ProjectSharedNotification {
-    type Event = ();
-}
-
-impl View for ProjectSharedNotification {
-    fn ui_name() -> &'static str {
-        "ProjectSharedNotification"
-    }
-
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> gpui::AnyElement<Self> {
-        let background = theme::current(cx).project_shared_notification.background;
-        Flex::row()
-            .with_child(self.render_owner(cx))
-            .with_child(self.render_buttons(cx))
-            .contained()
-            .with_background_color(background)
-            .expanded()
-            .into_any()
     }
 }

crates/collab_ui/src/panel_settings.rs 🔗

@@ -1,28 +1,29 @@
 use anyhow;
+use gpui::Pixels;
 use schemars::JsonSchema;
 use serde_derive::{Deserialize, Serialize};
-use settings::Setting;
+use settings::Settings;
 use workspace::dock::DockPosition;
 
 #[derive(Deserialize, Debug)]
 pub struct CollaborationPanelSettings {
     pub button: bool,
     pub dock: DockPosition,
-    pub default_width: f32,
+    pub default_width: Pixels,
 }
 
 #[derive(Deserialize, Debug)]
 pub struct ChatPanelSettings {
     pub button: bool,
     pub dock: DockPosition,
-    pub default_width: f32,
+    pub default_width: Pixels,
 }
 
 #[derive(Deserialize, Debug)]
 pub struct NotificationPanelSettings {
     pub button: bool,
     pub dock: DockPosition,
-    pub default_width: f32,
+    pub default_width: Pixels,
 }
 
 #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
@@ -32,37 +33,37 @@ pub struct PanelSettingsContent {
     pub default_width: Option<f32>,
 }
 
-impl Setting for CollaborationPanelSettings {
+impl Settings for CollaborationPanelSettings {
     const KEY: Option<&'static str> = Some("collaboration_panel");
     type FileContent = PanelSettingsContent;
     fn load(
         default_value: &Self::FileContent,
         user_values: &[&Self::FileContent],
-        _: &gpui::AppContext,
+        _: &mut gpui::AppContext,
     ) -> anyhow::Result<Self> {
         Self::load_via_json_merge(default_value, user_values)
     }
 }
 
-impl Setting for ChatPanelSettings {
+impl Settings for ChatPanelSettings {
     const KEY: Option<&'static str> = Some("chat_panel");
     type FileContent = PanelSettingsContent;
     fn load(
         default_value: &Self::FileContent,
         user_values: &[&Self::FileContent],
-        _: &gpui::AppContext,
+        _: &mut gpui::AppContext,
     ) -> anyhow::Result<Self> {
         Self::load_via_json_merge(default_value, user_values)
     }
 }
 
-impl Setting for NotificationPanelSettings {
+impl Settings for NotificationPanelSettings {
     const KEY: Option<&'static str> = Some("notification_panel");
     type FileContent = PanelSettingsContent;
     fn load(
         default_value: &Self::FileContent,
         user_values: &[&Self::FileContent],
-        _: &gpui::AppContext,
+        _: &mut gpui::AppContext,
     ) -> anyhow::Result<Self> {
         Self::load_via_json_merge(default_value, user_values)
     }

crates/collab_ui2/Cargo.toml 🔗

@@ -1,81 +0,0 @@
-[package]
-name = "collab_ui2"
-version = "0.1.0"
-edition = "2021"
-publish = false
-
-[lib]
-path = "src/collab_ui.rs"
-doctest = false
-
-[features]
-test-support = [
-    "call/test-support",
-    "client/test-support",
-    "collections/test-support",
-    "editor/test-support",
-    "gpui/test-support",
-    "project/test-support",
-    "settings/test-support",
-    "util/test-support",
-    "workspace/test-support",
-]
-
-[dependencies]
-auto_update = { package = "auto_update2", path = "../auto_update2" }
-db = { package = "db2", path = "../db2" }
-call = { package = "call2", path = "../call2" }
-client = { package = "client2", path = "../client2" }
-channel = { package = "channel2", path = "../channel2" }
-clock = { path = "../clock" }
-collections = { path = "../collections" }
-# context_menu = { path = "../context_menu" }
-# drag_and_drop = { path = "../drag_and_drop" }
-editor = { package="editor2", path = "../editor2" }
-feedback = { package = "feedback2", path = "../feedback2" }
-fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
-gpui = { package = "gpui2", path = "../gpui2" }
-language = { package = "language2", path = "../language2" }
-menu = { package = "menu2",  path = "../menu2" }
-notifications = { package = "notifications2",  path = "../notifications2" }
-rich_text = { package = "rich_text2", path = "../rich_text2" }
-picker = { package = "picker2", path = "../picker2" }
-project = { package = "project2", path = "../project2" }
-recent_projects = { package = "recent_projects2", path = "../recent_projects2" }
-rpc = { package ="rpc2",  path = "../rpc2" }
-settings = { package = "settings2", path = "../settings2" }
-feature_flags = { package = "feature_flags2", path = "../feature_flags2"}
-theme = { package = "theme2", path = "../theme2" }
-theme_selector = { package = "theme_selector2", path = "../theme_selector2" }
-vcs_menu = { package = "vcs_menu2", path = "../vcs_menu2" }
-ui = { package = "ui2", path = "../ui2" }
-util = { path = "../util" }
-workspace = { package = "workspace2", path = "../workspace2" }
-zed-actions = { package="zed_actions2", path = "../zed_actions2"}
-
-anyhow.workspace = true
-futures.workspace = true
-lazy_static.workspace = true
-log.workspace = true
-schemars.workspace = true
-postage.workspace = true
-serde.workspace = true
-serde_derive.workspace = true
-time.workspace = true
-smallvec.workspace = true
-
-[dev-dependencies]
-call = { package = "call2", path = "../call2", features = ["test-support"] }
-client = { package = "client2", path = "../client2", features = ["test-support"] }
-collections = { path = "../collections", features = ["test-support"] }
-editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
-gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
-notifications = { package = "notifications2", path = "../notifications2", features = ["test-support"] }
-project = { package = "project2", path = "../project2", features = ["test-support"] }
-rpc = { package = "rpc2", path = "../rpc2", features = ["test-support"] }
-settings = { package = "settings2", path = "../settings2", features = ["test-support"] }
-util = { path = "../util", features = ["test-support"] }
-workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
-
-pretty_assertions.workspace = true
-tree-sitter-markdown.workspace = true

crates/collab_ui2/src/channel_view.rs 🔗

@@ -1,448 +0,0 @@
-use anyhow::Result;
-use call::report_call_event_for_channel;
-use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId, ChannelStore};
-use client::{
-    proto::{self, PeerId},
-    Collaborator, ParticipantIndex,
-};
-use collections::HashMap;
-use editor::{CollaborationHub, Editor, EditorEvent};
-use gpui::{
-    actions, AnyElement, AnyView, AppContext, Entity as _, EventEmitter, FocusableView,
-    IntoElement as _, Model, Pixels, Point, Render, Subscription, Task, View, ViewContext,
-    VisualContext as _, WindowContext,
-};
-use project::Project;
-use std::{
-    any::{Any, TypeId},
-    sync::Arc,
-};
-use ui::{prelude::*, Label};
-use util::ResultExt;
-use workspace::{
-    item::{FollowableItem, Item, ItemEvent, ItemHandle},
-    register_followable_item,
-    searchable::SearchableItemHandle,
-    ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId,
-};
-
-actions!(collab, [Deploy]);
-
-pub fn init(cx: &mut AppContext) {
-    register_followable_item::<ChannelView>(cx)
-}
-
-pub struct ChannelView {
-    pub editor: View<Editor>,
-    project: Model<Project>,
-    channel_store: Model<ChannelStore>,
-    channel_buffer: Model<ChannelBuffer>,
-    remote_id: Option<ViewId>,
-    _editor_event_subscription: Subscription,
-}
-
-impl ChannelView {
-    pub fn open(
-        channel_id: ChannelId,
-        workspace: View<Workspace>,
-        cx: &mut WindowContext,
-    ) -> Task<Result<View<Self>>> {
-        let pane = workspace.read(cx).active_pane().clone();
-        let channel_view = Self::open_in_pane(channel_id, pane.clone(), workspace.clone(), cx);
-        cx.spawn(|mut cx| async move {
-            let channel_view = channel_view.await?;
-            pane.update(&mut cx, |pane, cx| {
-                report_call_event_for_channel(
-                    "open channel notes",
-                    channel_id,
-                    &workspace.read(cx).app_state().client,
-                    cx,
-                );
-                pane.add_item(Box::new(channel_view.clone()), true, true, None, cx);
-            })?;
-            anyhow::Ok(channel_view)
-        })
-    }
-
-    pub fn open_in_pane(
-        channel_id: ChannelId,
-        pane: View<Pane>,
-        workspace: View<Workspace>,
-        cx: &mut WindowContext,
-    ) -> Task<Result<View<Self>>> {
-        let workspace = workspace.read(cx);
-        let project = workspace.project().to_owned();
-        let channel_store = ChannelStore::global(cx);
-        let language_registry = workspace.app_state().languages.clone();
-        let markdown = language_registry.language_for_name("Markdown");
-        let channel_buffer =
-            channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx));
-
-        cx.spawn(|mut cx| async move {
-            let channel_buffer = channel_buffer.await?;
-            let markdown = markdown.await.log_err();
-
-            channel_buffer.update(&mut cx, |buffer, cx| {
-                buffer.buffer().update(cx, |buffer, cx| {
-                    buffer.set_language_registry(language_registry);
-                    if let Some(markdown) = markdown {
-                        buffer.set_language(Some(markdown), cx);
-                    }
-                })
-            })?;
-
-            pane.update(&mut cx, |pane, cx| {
-                let buffer_id = channel_buffer.read(cx).remote_id(cx);
-
-                let existing_view = pane
-                    .items_of_type::<Self>()
-                    .find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id);
-
-                // If this channel buffer is already open in this pane, just return it.
-                if let Some(existing_view) = existing_view.clone() {
-                    if existing_view.read(cx).channel_buffer == channel_buffer {
-                        return existing_view;
-                    }
-                }
-
-                let view = cx.new_view(|cx| {
-                    let mut this = Self::new(project, channel_store, channel_buffer, cx);
-                    this.acknowledge_buffer_version(cx);
-                    this
-                });
-
-                // If the pane contained a disconnected view for this channel buffer,
-                // replace that.
-                if let Some(existing_item) = existing_view {
-                    if let Some(ix) = pane.index_for_item(&existing_item) {
-                        pane.close_item_by_id(existing_item.entity_id(), SaveIntent::Skip, cx)
-                            .detach();
-                        pane.add_item(Box::new(view.clone()), true, true, Some(ix), cx);
-                    }
-                }
-
-                view
-            })
-        })
-    }
-
-    pub fn new(
-        project: Model<Project>,
-        channel_store: Model<ChannelStore>,
-        channel_buffer: Model<ChannelBuffer>,
-        cx: &mut ViewContext<Self>,
-    ) -> Self {
-        let buffer = channel_buffer.read(cx).buffer();
-        let editor = cx.new_view(|cx| {
-            let mut editor = Editor::for_buffer(buffer, None, cx);
-            editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub(
-                channel_buffer.clone(),
-            )));
-            editor.set_read_only(
-                !channel_buffer
-                    .read(cx)
-                    .channel(cx)
-                    .is_some_and(|c| c.can_edit_notes()),
-            );
-            editor
-        });
-        let _editor_event_subscription =
-            cx.subscribe(&editor, |_, _, e: &EditorEvent, cx| cx.emit(e.clone()));
-
-        cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event)
-            .detach();
-
-        Self {
-            editor,
-            project,
-            channel_store,
-            channel_buffer,
-            remote_id: None,
-            _editor_event_subscription,
-        }
-    }
-
-    pub fn channel(&self, cx: &AppContext) -> Option<Arc<Channel>> {
-        self.channel_buffer.read(cx).channel(cx)
-    }
-
-    fn handle_channel_buffer_event(
-        &mut self,
-        _: Model<ChannelBuffer>,
-        event: &ChannelBufferEvent,
-        cx: &mut ViewContext<Self>,
-    ) {
-        match event {
-            ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| {
-                editor.set_read_only(true);
-                cx.notify();
-            }),
-            ChannelBufferEvent::ChannelChanged => {
-                self.editor.update(cx, |editor, cx| {
-                    editor.set_read_only(!self.channel(cx).is_some_and(|c| c.can_edit_notes()));
-                    cx.emit(editor::EditorEvent::TitleChanged);
-                    cx.notify()
-                });
-            }
-            ChannelBufferEvent::BufferEdited => {
-                if self.editor.read(cx).is_focused(cx) {
-                    self.acknowledge_buffer_version(cx);
-                } else {
-                    self.channel_store.update(cx, |store, cx| {
-                        let channel_buffer = self.channel_buffer.read(cx);
-                        store.notes_changed(
-                            channel_buffer.channel_id,
-                            channel_buffer.epoch(),
-                            &channel_buffer.buffer().read(cx).version(),
-                            cx,
-                        )
-                    });
-                }
-            }
-            ChannelBufferEvent::CollaboratorsChanged => {}
-        }
-    }
-
-    fn acknowledge_buffer_version(&mut self, cx: &mut ViewContext<ChannelView>) {
-        self.channel_store.update(cx, |store, cx| {
-            let channel_buffer = self.channel_buffer.read(cx);
-            store.acknowledge_notes_version(
-                channel_buffer.channel_id,
-                channel_buffer.epoch(),
-                &channel_buffer.buffer().read(cx).version(),
-                cx,
-            )
-        });
-        self.channel_buffer.update(cx, |buffer, cx| {
-            buffer.acknowledge_buffer_version(cx);
-        });
-    }
-}
-
-impl EventEmitter<EditorEvent> for ChannelView {}
-
-impl Render for ChannelView {
-    fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
-        self.editor.clone()
-    }
-}
-
-impl FocusableView for ChannelView {
-    fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
-        self.editor.read(cx).focus_handle(cx)
-    }
-}
-
-impl Item for ChannelView {
-    type Event = EditorEvent;
-
-    fn act_as_type<'a>(
-        &'a self,
-        type_id: TypeId,
-        self_handle: &'a View<Self>,
-        _: &'a AppContext,
-    ) -> Option<AnyView> {
-        if type_id == TypeId::of::<Self>() {
-            Some(self_handle.to_any())
-        } else if type_id == TypeId::of::<Editor>() {
-            Some(self.editor.to_any())
-        } else {
-            None
-        }
-    }
-
-    fn tab_content(&self, _: Option<usize>, selected: bool, cx: &WindowContext) -> AnyElement {
-        let label = if let Some(channel) = self.channel(cx) {
-            match (
-                channel.can_edit_notes(),
-                self.channel_buffer.read(cx).is_connected(),
-            ) {
-                (true, true) => format!("#{}", channel.name),
-                (false, true) => format!("#{} (read-only)", channel.name),
-                (_, false) => format!("#{} (disconnected)", channel.name),
-            }
-        } else {
-            format!("channel notes (disconnected)")
-        };
-        Label::new(label)
-            .color(if selected {
-                Color::Default
-            } else {
-                Color::Muted
-            })
-            .into_any_element()
-    }
-
-    fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<View<Self>> {
-        Some(cx.new_view(|cx| {
-            Self::new(
-                self.project.clone(),
-                self.channel_store.clone(),
-                self.channel_buffer.clone(),
-                cx,
-            )
-        }))
-    }
-
-    fn is_singleton(&self, _cx: &AppContext) -> bool {
-        false
-    }
-
-    fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
-        self.editor
-            .update(cx, |editor, cx| editor.navigate(data, cx))
-    }
-
-    fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
-        self.editor
-            .update(cx, |editor, cx| Item::deactivated(editor, cx))
-    }
-
-    fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext<Self>) {
-        self.editor
-            .update(cx, |editor, cx| Item::set_nav_history(editor, history, cx))
-    }
-
-    fn as_searchable(&self, _: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
-        Some(Box::new(self.editor.clone()))
-    }
-
-    fn show_toolbar(&self) -> bool {
-        true
-    }
-
-    fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Point<Pixels>> {
-        self.editor.read(cx).pixel_position_of_cursor(cx)
-    }
-
-    fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
-        Editor::to_item_events(event, f)
-    }
-}
-
-impl FollowableItem for ChannelView {
-    fn remote_id(&self) -> Option<workspace::ViewId> {
-        self.remote_id
-    }
-
-    fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
-        let channel_buffer = self.channel_buffer.read(cx);
-        if !channel_buffer.is_connected() {
-            return None;
-        }
-
-        Some(proto::view::Variant::ChannelView(
-            proto::view::ChannelView {
-                channel_id: channel_buffer.channel_id,
-                editor: if let Some(proto::view::Variant::Editor(proto)) =
-                    self.editor.read(cx).to_state_proto(cx)
-                {
-                    Some(proto)
-                } else {
-                    None
-                },
-            },
-        ))
-    }
-
-    fn from_state_proto(
-        pane: View<workspace::Pane>,
-        workspace: View<workspace::Workspace>,
-        remote_id: workspace::ViewId,
-        state: &mut Option<proto::view::Variant>,
-        cx: &mut WindowContext,
-    ) -> Option<gpui::Task<anyhow::Result<View<Self>>>> {
-        let Some(proto::view::Variant::ChannelView(_)) = state else {
-            return None;
-        };
-        let Some(proto::view::Variant::ChannelView(state)) = state.take() else {
-            unreachable!()
-        };
-
-        let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx);
-
-        Some(cx.spawn(|mut cx| async move {
-            let this = open.await?;
-
-            let task = this.update(&mut cx, |this, cx| {
-                this.remote_id = Some(remote_id);
-
-                if let Some(state) = state.editor {
-                    Some(this.editor.update(cx, |editor, cx| {
-                        editor.apply_update_proto(
-                            &this.project,
-                            proto::update_view::Variant::Editor(proto::update_view::Editor {
-                                selections: state.selections,
-                                pending_selection: state.pending_selection,
-                                scroll_top_anchor: state.scroll_top_anchor,
-                                scroll_x: state.scroll_x,
-                                scroll_y: state.scroll_y,
-                                ..Default::default()
-                            }),
-                            cx,
-                        )
-                    }))
-                } else {
-                    None
-                }
-            })?;
-
-            if let Some(task) = task {
-                task.await?;
-            }
-
-            Ok(this)
-        }))
-    }
-
-    fn add_event_to_update_proto(
-        &self,
-        event: &EditorEvent,
-        update: &mut Option<proto::update_view::Variant>,
-        cx: &WindowContext,
-    ) -> bool {
-        self.editor
-            .read(cx)
-            .add_event_to_update_proto(event, update, cx)
-    }
-
-    fn apply_update_proto(
-        &mut self,
-        project: &Model<Project>,
-        message: proto::update_view::Variant,
-        cx: &mut ViewContext<Self>,
-    ) -> gpui::Task<anyhow::Result<()>> {
-        self.editor.update(cx, |editor, cx| {
-            editor.apply_update_proto(project, message, cx)
-        })
-    }
-
-    fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
-        self.editor.update(cx, |editor, cx| {
-            editor.set_leader_peer_id(leader_peer_id, cx)
-        })
-    }
-
-    fn is_project_item(&self, _cx: &WindowContext) -> bool {
-        false
-    }
-
-    fn to_follow_event(event: &Self::Event) -> Option<workspace::item::FollowEvent> {
-        Editor::to_follow_event(event)
-    }
-}
-
-struct ChannelBufferCollaborationHub(Model<ChannelBuffer>);
-
-impl CollaborationHub for ChannelBufferCollaborationHub {
-    fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator> {
-        self.0.read(cx).collaborators()
-    }
-
-    fn user_participant_indices<'a>(
-        &self,
-        cx: &'a AppContext,
-    ) -> &'a HashMap<u64, ParticipantIndex> {
-        self.0.read(cx).user_store().read(cx).participant_indices()
-    }
-}

crates/collab_ui2/src/chat_panel.rs 🔗

@@ -1,704 +0,0 @@
-use crate::{channel_view::ChannelView, is_channels_feature_enabled, ChatPanelSettings};
-use anyhow::Result;
-use call::ActiveCall;
-use channel::{ChannelChat, ChannelChatEvent, ChannelMessageId, ChannelStore};
-use client::Client;
-use collections::HashMap;
-use db::kvp::KEY_VALUE_STORE;
-use editor::Editor;
-use gpui::{
-    actions, div, list, prelude::*, px, serde_json, AnyElement, AppContext, AsyncWindowContext,
-    ClickEvent, ElementId, EventEmitter, FocusableView, ListOffset, ListScrollEvent, ListState,
-    Model, Render, Subscription, Task, View, ViewContext, VisualContext, WeakView,
-};
-use language::LanguageRegistry;
-use menu::Confirm;
-use message_editor::MessageEditor;
-use project::Fs;
-use rich_text::RichText;
-use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsStore};
-use std::sync::Arc;
-use theme::ActiveTheme as _;
-use time::{OffsetDateTime, UtcOffset};
-use ui::{prelude::*, Avatar, Button, Icon, IconButton, Label, TabBar, Tooltip};
-use util::{ResultExt, TryFutureExt};
-use workspace::{
-    dock::{DockPosition, Panel, PanelEvent},
-    Workspace,
-};
-
-mod message_editor;
-
-const MESSAGE_LOADING_THRESHOLD: usize = 50;
-const CHAT_PANEL_KEY: &'static str = "ChatPanel";
-
-pub fn init(cx: &mut AppContext) {
-    cx.observe_new_views(|workspace: &mut Workspace, _| {
-        workspace.register_action(|workspace, _: &ToggleFocus, cx| {
-            workspace.toggle_panel_focus::<ChatPanel>(cx);
-        });
-    })
-    .detach();
-}
-
-pub struct ChatPanel {
-    client: Arc<Client>,
-    channel_store: Model<ChannelStore>,
-    languages: Arc<LanguageRegistry>,
-    message_list: ListState,
-    active_chat: Option<(Model<ChannelChat>, Subscription)>,
-    input_editor: View<MessageEditor>,
-    local_timezone: UtcOffset,
-    fs: Arc<dyn Fs>,
-    width: Option<Pixels>,
-    active: bool,
-    pending_serialization: Task<Option<()>>,
-    subscriptions: Vec<gpui::Subscription>,
-    workspace: WeakView<Workspace>,
-    is_scrolled_to_bottom: bool,
-    markdown_data: HashMap<ChannelMessageId, RichText>,
-}
-
-#[derive(Serialize, Deserialize)]
-struct SerializedChatPanel {
-    width: Option<Pixels>,
-}
-
-#[derive(Debug)]
-pub enum Event {
-    DockPositionChanged,
-    Focus,
-    Dismissed,
-}
-
-actions!(chat_panel, [ToggleFocus]);
-
-impl ChatPanel {
-    pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
-        let fs = workspace.app_state().fs.clone();
-        let client = workspace.app_state().client.clone();
-        let channel_store = ChannelStore::global(cx);
-        let languages = workspace.app_state().languages.clone();
-
-        let input_editor = cx.new_view(|cx| {
-            MessageEditor::new(
-                languages.clone(),
-                channel_store.clone(),
-                cx.new_view(|cx| Editor::auto_height(4, cx)),
-                cx,
-            )
-        });
-
-        let workspace_handle = workspace.weak_handle();
-
-        cx.new_view(|cx: &mut ViewContext<Self>| {
-            let view = cx.view().downgrade();
-            let message_list =
-                ListState::new(0, gpui::ListAlignment::Bottom, px(1000.), move |ix, cx| {
-                    if let Some(view) = view.upgrade() {
-                        view.update(cx, |view, cx| {
-                            view.render_message(ix, cx).into_any_element()
-                        })
-                    } else {
-                        div().into_any()
-                    }
-                });
-
-            message_list.set_scroll_handler(cx.listener(|this, event: &ListScrollEvent, cx| {
-                if event.visible_range.start < MESSAGE_LOADING_THRESHOLD {
-                    this.load_more_messages(cx);
-                }
-                this.is_scrolled_to_bottom = event.visible_range.end == event.count;
-            }));
-
-            let mut this = Self {
-                fs,
-                client,
-                channel_store,
-                languages,
-                message_list,
-                active_chat: Default::default(),
-                pending_serialization: Task::ready(None),
-                input_editor,
-                local_timezone: cx.local_timezone(),
-                subscriptions: Vec::new(),
-                workspace: workspace_handle,
-                is_scrolled_to_bottom: true,
-                active: false,
-                width: None,
-                markdown_data: Default::default(),
-            };
-
-            let mut old_dock_position = this.position(cx);
-            this.subscriptions.push(cx.observe_global::<SettingsStore>(
-                move |this: &mut Self, cx| {
-                    let new_dock_position = this.position(cx);
-                    if new_dock_position != old_dock_position {
-                        old_dock_position = new_dock_position;
-                        cx.emit(Event::DockPositionChanged);
-                    }
-                    cx.notify();
-                },
-            ));
-
-            this
-        })
-    }
-
-    pub fn is_scrolled_to_bottom(&self) -> bool {
-        self.is_scrolled_to_bottom
-    }
-
-    pub fn active_chat(&self) -> Option<Model<ChannelChat>> {
-        self.active_chat.as_ref().map(|(chat, _)| chat.clone())
-    }
-
-    pub fn load(
-        workspace: WeakView<Workspace>,
-        cx: AsyncWindowContext,
-    ) -> Task<Result<View<Self>>> {
-        cx.spawn(|mut cx| async move {
-            let serialized_panel = if let Some(panel) = cx
-                .background_executor()
-                .spawn(async move { KEY_VALUE_STORE.read_kvp(CHAT_PANEL_KEY) })
-                .await
-                .log_err()
-                .flatten()
-            {
-                Some(serde_json::from_str::<SerializedChatPanel>(&panel)?)
-            } else {
-                None
-            };
-
-            workspace.update(&mut cx, |workspace, cx| {
-                let panel = Self::new(workspace, cx);
-                if let Some(serialized_panel) = serialized_panel {
-                    panel.update(cx, |panel, cx| {
-                        panel.width = serialized_panel.width;
-                        cx.notify();
-                    });
-                }
-                panel
-            })
-        })
-    }
-
-    fn serialize(&mut self, cx: &mut ViewContext<Self>) {
-        let width = self.width;
-        self.pending_serialization = cx.background_executor().spawn(
-            async move {
-                KEY_VALUE_STORE
-                    .write_kvp(
-                        CHAT_PANEL_KEY.into(),
-                        serde_json::to_string(&SerializedChatPanel { width })?,
-                    )
-                    .await?;
-                anyhow::Ok(())
-            }
-            .log_err(),
-        );
-    }
-
-    fn set_active_chat(&mut self, chat: Model<ChannelChat>, cx: &mut ViewContext<Self>) {
-        if self.active_chat.as_ref().map(|e| &e.0) != Some(&chat) {
-            let channel_id = chat.read(cx).channel_id;
-            {
-                self.markdown_data.clear();
-                let chat = chat.read(cx);
-                self.message_list.reset(chat.message_count());
-
-                let channel_name = chat.channel(cx).map(|channel| channel.name.clone());
-                self.input_editor.update(cx, |editor, cx| {
-                    editor.set_channel(channel_id, channel_name, cx);
-                });
-            };
-            let subscription = cx.subscribe(&chat, Self::channel_did_change);
-            self.active_chat = Some((chat, subscription));
-            self.acknowledge_last_message(cx);
-            cx.notify();
-        }
-    }
-
-    fn channel_did_change(
-        &mut self,
-        _: Model<ChannelChat>,
-        event: &ChannelChatEvent,
-        cx: &mut ViewContext<Self>,
-    ) {
-        match event {
-            ChannelChatEvent::MessagesUpdated {
-                old_range,
-                new_count,
-            } => {
-                self.message_list.splice(old_range.clone(), *new_count);
-                if self.active {
-                    self.acknowledge_last_message(cx);
-                }
-            }
-            ChannelChatEvent::NewMessage {
-                channel_id,
-                message_id,
-            } => {
-                if !self.active {
-                    self.channel_store.update(cx, |store, cx| {
-                        store.new_message(*channel_id, *message_id, cx)
-                    })
-                }
-            }
-        }
-        cx.notify();
-    }
-
-    fn acknowledge_last_message(&mut self, cx: &mut ViewContext<Self>) {
-        if self.active && self.is_scrolled_to_bottom {
-            if let Some((chat, _)) = &self.active_chat {
-                chat.update(cx, |chat, cx| {
-                    chat.acknowledge_last_message(cx);
-                });
-            }
-        }
-    }
-
-    fn render_channel(&self, cx: &mut ViewContext<Self>) -> AnyElement {
-        v_stack()
-            .full()
-            .on_action(cx.listener(Self::send))
-            .child(
-                h_stack().z_index(1).child(
-                    TabBar::new("chat_header")
-                        .child(
-                            h_stack()
-                                .w_full()
-                                .h(rems(ui::Tab::HEIGHT_IN_REMS))
-                                .px_2()
-                                .child(Label::new(
-                                    self.active_chat
-                                        .as_ref()
-                                        .and_then(|c| {
-                                            Some(format!("#{}", c.0.read(cx).channel(cx)?.name))
-                                        })
-                                        .unwrap_or_default(),
-                                )),
-                        )
-                        .end_child(
-                            IconButton::new("notes", Icon::File)
-                                .on_click(cx.listener(Self::open_notes))
-                                .tooltip(|cx| Tooltip::text("Open notes", cx)),
-                        )
-                        .end_child(
-                            IconButton::new("call", Icon::AudioOn)
-                                .on_click(cx.listener(Self::join_call))
-                                .tooltip(|cx| Tooltip::text("Join call", cx)),
-                        ),
-                ),
-            )
-            .child(div().flex_grow().px_2().py_1().map(|this| {
-                if self.active_chat.is_some() {
-                    this.child(list(self.message_list.clone()).full())
-                } else {
-                    this
-                }
-            }))
-            .child(
-                div()
-                    .z_index(1)
-                    .p_2()
-                    .bg(cx.theme().colors().background)
-                    .child(self.input_editor.clone()),
-            )
-            .into_any()
-    }
-
-    fn render_message(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> impl IntoElement {
-        let active_chat = &self.active_chat.as_ref().unwrap().0;
-        let (message, is_continuation_from_previous, is_continuation_to_next, is_admin) =
-            active_chat.update(cx, |active_chat, cx| {
-                let is_admin = self
-                    .channel_store
-                    .read(cx)
-                    .is_channel_admin(active_chat.channel_id);
-
-                let last_message = active_chat.message(ix.saturating_sub(1));
-                let this_message = active_chat.message(ix).clone();
-                let next_message =
-                    active_chat.message(ix.saturating_add(1).min(active_chat.message_count() - 1));
-
-                let is_continuation_from_previous = last_message.id != this_message.id
-                    && last_message.sender.id == this_message.sender.id;
-                let is_continuation_to_next = this_message.id != next_message.id
-                    && this_message.sender.id == next_message.sender.id;
-
-                if let ChannelMessageId::Saved(id) = this_message.id {
-                    if this_message
-                        .mentions
-                        .iter()
-                        .any(|(_, user_id)| Some(*user_id) == self.client.user_id())
-                    {
-                        active_chat.acknowledge_message(id);
-                    }
-                }
-
-                (
-                    this_message,
-                    is_continuation_from_previous,
-                    is_continuation_to_next,
-                    is_admin,
-                )
-            });
-
-        let _is_pending = message.is_pending();
-        let text = self.markdown_data.entry(message.id).or_insert_with(|| {
-            Self::render_markdown_with_mentions(&self.languages, self.client.id(), &message)
-        });
-
-        let now = OffsetDateTime::now_utc();
-
-        let belongs_to_user = Some(message.sender.id) == self.client.user_id();
-        let message_id_to_remove = if let (ChannelMessageId::Saved(id), true) =
-            (message.id, belongs_to_user || is_admin)
-        {
-            Some(id)
-        } else {
-            None
-        };
-
-        let element_id: ElementId = match message.id {
-            ChannelMessageId::Saved(id) => ("saved-message", id).into(),
-            ChannelMessageId::Pending(id) => ("pending-message", id).into(),
-        };
-
-        v_stack()
-            .w_full()
-            .id(element_id)
-            .relative()
-            .overflow_hidden()
-            .group("")
-            .when(!is_continuation_from_previous, |this| {
-                this.child(
-                    h_stack()
-                        .gap_2()
-                        .child(Avatar::new(message.sender.avatar_uri.clone()))
-                        .child(Label::new(message.sender.github_login.clone()))
-                        .child(
-                            Label::new(format_timestamp(
-                                message.timestamp,
-                                now,
-                                self.local_timezone,
-                            ))
-                            .color(Color::Muted),
-                        ),
-                )
-            })
-            .when(!is_continuation_to_next, |this|
-                // HACK: This should really be a margin, but margins seem to get collapsed.
-                this.pb_2())
-            .child(text.element("body".into(), cx))
-            .child(
-                div()
-                    .absolute()
-                    .top_1()
-                    .right_2()
-                    .w_8()
-                    .visible_on_hover("")
-                    .children(message_id_to_remove.map(|message_id| {
-                        IconButton::new(("remove", message_id), Icon::XCircle).on_click(
-                            cx.listener(move |this, _, cx| {
-                                this.remove_message(message_id, cx);
-                            }),
-                        )
-                    })),
-            )
-    }
-
-    fn render_markdown_with_mentions(
-        language_registry: &Arc<LanguageRegistry>,
-        current_user_id: u64,
-        message: &channel::ChannelMessage,
-    ) -> RichText {
-        let mentions = message
-            .mentions
-            .iter()
-            .map(|(range, user_id)| rich_text::Mention {
-                range: range.clone(),
-                is_self_mention: *user_id == current_user_id,
-            })
-            .collect::<Vec<_>>();
-
-        rich_text::render_markdown(message.body.clone(), &mentions, language_registry, None)
-    }
-
-    fn render_sign_in_prompt(&self, cx: &mut ViewContext<Self>) -> AnyElement {
-        Button::new("sign-in", "Sign in to use chat")
-            .on_click(cx.listener(move |this, _, cx| {
-                let client = this.client.clone();
-                cx.spawn(|this, mut cx| async move {
-                    if client
-                        .authenticate_and_connect(true, &cx)
-                        .log_err()
-                        .await
-                        .is_some()
-                    {
-                        this.update(&mut cx, |_, cx| {
-                            cx.focus_self();
-                        })
-                        .ok();
-                    }
-                })
-                .detach();
-            }))
-            .into_any_element()
-    }
-
-    fn send(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
-        if let Some((chat, _)) = self.active_chat.as_ref() {
-            let message = self
-                .input_editor
-                .update(cx, |editor, cx| editor.take_message(cx));
-
-            if let Some(task) = chat
-                .update(cx, |chat, cx| chat.send_message(message, cx))
-                .log_err()
-            {
-                task.detach();
-            }
-        }
-    }
-
-    fn remove_message(&mut self, id: u64, cx: &mut ViewContext<Self>) {
-        if let Some((chat, _)) = self.active_chat.as_ref() {
-            chat.update(cx, |chat, cx| chat.remove_message(id, cx).detach())
-        }
-    }
-
-    fn load_more_messages(&mut self, cx: &mut ViewContext<Self>) {
-        if let Some((chat, _)) = self.active_chat.as_ref() {
-            chat.update(cx, |channel, cx| {
-                if let Some(task) = channel.load_more_messages(cx) {
-                    task.detach();
-                }
-            })
-        }
-    }
-
-    pub fn select_channel(
-        &mut self,
-        selected_channel_id: u64,
-        scroll_to_message_id: Option<u64>,
-        cx: &mut ViewContext<ChatPanel>,
-    ) -> Task<Result<()>> {
-        let open_chat = self
-            .active_chat
-            .as_ref()
-            .and_then(|(chat, _)| {
-                (chat.read(cx).channel_id == selected_channel_id)
-                    .then(|| Task::ready(anyhow::Ok(chat.clone())))
-            })
-            .unwrap_or_else(|| {
-                self.channel_store.update(cx, |store, cx| {
-                    store.open_channel_chat(selected_channel_id, cx)
-                })
-            });
-
-        cx.spawn(|this, mut cx| async move {
-            let chat = open_chat.await?;
-            this.update(&mut cx, |this, cx| {
-                this.set_active_chat(chat.clone(), cx);
-            })?;
-
-            if let Some(message_id) = scroll_to_message_id {
-                if let Some(item_ix) =
-                    ChannelChat::load_history_since_message(chat.clone(), message_id, (*cx).clone())
-                        .await
-                {
-                    this.update(&mut cx, |this, cx| {
-                        if this.active_chat.as_ref().map_or(false, |(c, _)| *c == chat) {
-                            this.message_list.scroll_to(ListOffset {
-                                item_ix,
-                                offset_in_item: px(0.0),
-                            });
-                            cx.notify();
-                        }
-                    })?;
-                }
-            }
-
-            Ok(())
-        })
-    }
-
-    fn open_notes(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
-        if let Some((chat, _)) = &self.active_chat {
-            let channel_id = chat.read(cx).channel_id;
-            if let Some(workspace) = self.workspace.upgrade() {
-                ChannelView::open(channel_id, workspace, cx).detach();
-            }
-        }
-    }
-
-    fn join_call(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
-        if let Some((chat, _)) = &self.active_chat {
-            let channel_id = chat.read(cx).channel_id;
-            ActiveCall::global(cx)
-                .update(cx, |call, cx| call.join_channel(channel_id, cx))
-                .detach_and_log_err(cx);
-        }
-    }
-}
-
-impl EventEmitter<Event> for ChatPanel {}
-
-impl Render for ChatPanel {
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
-        div()
-            .full()
-            .child(if self.client.user_id().is_some() {
-                self.render_channel(cx)
-            } else {
-                self.render_sign_in_prompt(cx)
-            })
-            .min_w(px(150.))
-    }
-}
-
-impl FocusableView for ChatPanel {
-    fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
-        self.input_editor.read(cx).focus_handle(cx)
-    }
-}
-
-impl Panel for ChatPanel {
-    fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
-        ChatPanelSettings::get_global(cx).dock
-    }
-
-    fn position_is_valid(&self, position: DockPosition) -> bool {
-        matches!(position, DockPosition::Left | DockPosition::Right)
-    }
-
-    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
-        settings::update_settings_file::<ChatPanelSettings>(self.fs.clone(), cx, move |settings| {
-            settings.dock = Some(position)
-        });
-    }
-
-    fn size(&self, cx: &gpui::WindowContext) -> Pixels {
-        self.width
-            .unwrap_or_else(|| ChatPanelSettings::get_global(cx).default_width)
-    }
-
-    fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
-        self.width = size;
-        self.serialize(cx);
-        cx.notify();
-    }
-
-    fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
-        self.active = active;
-        if active {
-            self.acknowledge_last_message(cx);
-            if !is_channels_feature_enabled(cx) {
-                cx.emit(Event::Dismissed);
-            }
-        }
-    }
-
-    fn persistent_name() -> &'static str {
-        "ChatPanel"
-    }
-
-    fn icon(&self, _cx: &WindowContext) -> Option<ui::Icon> {
-        Some(ui::Icon::MessageBubbles)
-    }
-
-    fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
-        Some("Chat Panel")
-    }
-
-    fn toggle_action(&self) -> Box<dyn gpui::Action> {
-        Box::new(ToggleFocus)
-    }
-}
-
-impl EventEmitter<PanelEvent> for ChatPanel {}
-
-fn format_timestamp(
-    mut timestamp: OffsetDateTime,
-    mut now: OffsetDateTime,
-    local_timezone: UtcOffset,
-) -> String {
-    timestamp = timestamp.to_offset(local_timezone);
-    now = now.to_offset(local_timezone);
-
-    let today = now.date();
-    let date = timestamp.date();
-    let mut hour = timestamp.hour();
-    let mut part = "am";
-    if hour > 12 {
-        hour -= 12;
-        part = "pm";
-    }
-    if date == today {
-        format!("{:02}:{:02}{}", hour, timestamp.minute(), part)
-    } else if date.next_day() == Some(today) {
-        format!("yesterday at {:02}:{:02}{}", hour, timestamp.minute(), part)
-    } else {
-        format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year())
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use gpui::HighlightStyle;
-    use pretty_assertions::assert_eq;
-    use rich_text::Highlight;
-    use util::test::marked_text_ranges;
-
-    #[gpui::test]
-    fn test_render_markdown_with_mentions() {
-        let language_registry = Arc::new(LanguageRegistry::test());
-        let (body, ranges) = marked_text_ranges("*hi*, «@abc», let's **call** «@fgh»", false);
-        let message = channel::ChannelMessage {
-            id: ChannelMessageId::Saved(0),
-            body,
-            timestamp: OffsetDateTime::now_utc(),
-            sender: Arc::new(client::User {
-                github_login: "fgh".into(),
-                avatar_uri: "avatar_fgh".into(),
-                id: 103,
-            }),
-            nonce: 5,
-            mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)],
-        };
-
-        let message = ChatPanel::render_markdown_with_mentions(&language_registry, 102, &message);
-
-        // Note that the "'" was replaced with ’ due to smart punctuation.
-        let (body, ranges) = marked_text_ranges("«hi», «@abc», let’s «call» «@fgh»", false);
-        assert_eq!(message.text, body);
-        assert_eq!(
-            message.highlights,
-            vec![
-                (
-                    ranges[0].clone(),
-                    HighlightStyle {
-                        font_style: Some(gpui::FontStyle::Italic),
-                        ..Default::default()
-                    }
-                    .into()
-                ),
-                (ranges[1].clone(), Highlight::Mention),
-                (
-                    ranges[2].clone(),
-                    HighlightStyle {
-                        font_weight: Some(gpui::FontWeight::BOLD),
-                        ..Default::default()
-                    }
-                    .into()
-                ),
-                (ranges[3].clone(), Highlight::SelfMention)
-            ]
-        );
-    }
-}

crates/collab_ui2/src/chat_panel/message_editor.rs 🔗

@@ -1,296 +0,0 @@
-use channel::{ChannelId, ChannelMembership, ChannelStore, MessageParams};
-use client::UserId;
-use collections::HashMap;
-use editor::{AnchorRangeExt, Editor};
-use gpui::{
-    AsyncWindowContext, FocusableView, IntoElement, Model, Render, SharedString, Task, View,
-    ViewContext, WeakView,
-};
-use language::{language_settings::SoftWrap, Buffer, BufferSnapshot, LanguageRegistry};
-use lazy_static::lazy_static;
-use project::search::SearchQuery;
-use std::{sync::Arc, time::Duration};
-use workspace::item::ItemHandle;
-
-const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50);
-
-lazy_static! {
-    static ref MENTIONS_SEARCH: SearchQuery =
-        SearchQuery::regex("@[-_\\w]+", false, false, false, Vec::new(), Vec::new()).unwrap();
-}
-
-pub struct MessageEditor {
-    pub editor: View<Editor>,
-    channel_store: Model<ChannelStore>,
-    users: HashMap<String, UserId>,
-    mentions: Vec<UserId>,
-    mentions_task: Option<Task<()>>,
-    channel_id: Option<ChannelId>,
-}
-
-impl MessageEditor {
-    pub fn new(
-        language_registry: Arc<LanguageRegistry>,
-        channel_store: Model<ChannelStore>,
-        editor: View<Editor>,
-        cx: &mut ViewContext<Self>,
-    ) -> Self {
-        editor.update(cx, |editor, cx| {
-            editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
-        });
-
-        let buffer = editor
-            .read(cx)
-            .buffer()
-            .read(cx)
-            .as_singleton()
-            .expect("message editor must be singleton");
-
-        cx.subscribe(&buffer, Self::on_buffer_event).detach();
-
-        let markdown = language_registry.language_for_name("Markdown");
-        cx.spawn(|_, mut cx| async move {
-            let markdown = markdown.await?;
-            buffer.update(&mut cx, |buffer, cx| {
-                buffer.set_language(Some(markdown), cx)
-            })
-        })
-        .detach_and_log_err(cx);
-
-        Self {
-            editor,
-            channel_store,
-            users: HashMap::default(),
-            channel_id: None,
-            mentions: Vec::new(),
-            mentions_task: None,
-        }
-    }
-
-    pub fn set_channel(
-        &mut self,
-        channel_id: u64,
-        channel_name: Option<SharedString>,
-        cx: &mut ViewContext<Self>,
-    ) {
-        self.editor.update(cx, |editor, cx| {
-            if let Some(channel_name) = channel_name {
-                editor.set_placeholder_text(format!("Message #{}", channel_name), cx);
-            } else {
-                editor.set_placeholder_text(format!("Message Channel"), cx);
-            }
-        });
-        self.channel_id = Some(channel_id);
-        self.refresh_users(cx);
-    }
-
-    pub fn refresh_users(&mut self, cx: &mut ViewContext<Self>) {
-        if let Some(channel_id) = self.channel_id {
-            let members = self.channel_store.update(cx, |store, cx| {
-                store.get_channel_member_details(channel_id, cx)
-            });
-            cx.spawn(|this, mut cx| async move {
-                let members = members.await?;
-                this.update(&mut cx, |this, cx| this.set_members(members, cx))?;
-                anyhow::Ok(())
-            })
-            .detach_and_log_err(cx);
-        }
-    }
-
-    pub fn set_members(&mut self, members: Vec<ChannelMembership>, _: &mut ViewContext<Self>) {
-        self.users.clear();
-        self.users.extend(
-            members
-                .into_iter()
-                .map(|member| (member.user.github_login.clone(), member.user.id)),
-        );
-    }
-
-    pub fn take_message(&mut self, cx: &mut ViewContext<Self>) -> MessageParams {
-        self.editor.update(cx, |editor, cx| {
-            let highlights = editor.text_highlights::<Self>(cx);
-            let text = editor.text(cx);
-            let snapshot = editor.buffer().read(cx).snapshot(cx);
-            let mentions = if let Some((_, ranges)) = highlights {
-                ranges
-                    .iter()
-                    .map(|range| range.to_offset(&snapshot))
-                    .zip(self.mentions.iter().copied())
-                    .collect()
-            } else {
-                Vec::new()
-            };
-
-            editor.clear(cx);
-            self.mentions.clear();
-
-            MessageParams { text, mentions }
-        })
-    }
-
-    fn on_buffer_event(
-        &mut self,
-        buffer: Model<Buffer>,
-        event: &language::Event,
-        cx: &mut ViewContext<Self>,
-    ) {
-        if let language::Event::Reparsed | language::Event::Edited = event {
-            let buffer = buffer.read(cx).snapshot();
-            self.mentions_task = Some(cx.spawn(|this, cx| async move {
-                cx.background_executor()
-                    .timer(MENTIONS_DEBOUNCE_INTERVAL)
-                    .await;
-                Self::find_mentions(this, buffer, cx).await;
-            }));
-        }
-    }
-
-    async fn find_mentions(
-        this: WeakView<MessageEditor>,
-        buffer: BufferSnapshot,
-        mut cx: AsyncWindowContext,
-    ) {
-        let (buffer, ranges) = cx
-            .background_executor()
-            .spawn(async move {
-                let ranges = MENTIONS_SEARCH.search(&buffer, None).await;
-                (buffer, ranges)
-            })
-            .await;
-
-        this.update(&mut cx, |this, cx| {
-            let mut anchor_ranges = Vec::new();
-            let mut mentioned_user_ids = Vec::new();
-            let mut text = String::new();
-
-            this.editor.update(cx, |editor, cx| {
-                let multi_buffer = editor.buffer().read(cx).snapshot(cx);
-                for range in ranges {
-                    text.clear();
-                    text.extend(buffer.text_for_range(range.clone()));
-                    if let Some(username) = text.strip_prefix("@") {
-                        if let Some(user_id) = this.users.get(username) {
-                            let start = multi_buffer.anchor_after(range.start);
-                            let end = multi_buffer.anchor_after(range.end);
-
-                            mentioned_user_ids.push(*user_id);
-                            anchor_ranges.push(start..end);
-                        }
-                    }
-                }
-
-                editor.clear_highlights::<Self>(cx);
-                editor.highlight_text::<Self>(anchor_ranges, gpui::red().into(), cx)
-            });
-
-            this.mentions = mentioned_user_ids;
-            this.mentions_task.take();
-        })
-        .ok();
-    }
-
-    pub(crate) fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle {
-        self.editor.read(cx).focus_handle(cx)
-    }
-}
-
-impl Render for MessageEditor {
-    fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
-        self.editor.to_any()
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use client::{Client, User, UserStore};
-    use gpui::{Context as _, TestAppContext, VisualContext as _};
-    use language::{Language, LanguageConfig};
-    use rpc::proto;
-    use settings::SettingsStore;
-    use util::{http::FakeHttpClient, test::marked_text_ranges};
-
-    #[gpui::test]
-    async fn test_message_editor(cx: &mut TestAppContext) {
-        let language_registry = init_test(cx);
-
-        let (editor, cx) = cx.add_window_view(|cx| {
-            MessageEditor::new(
-                language_registry,
-                ChannelStore::global(cx),
-                cx.new_view(|cx| Editor::auto_height(4, cx)),
-                cx,
-            )
-        });
-        cx.executor().run_until_parked();
-
-        editor.update(cx, |editor, cx| {
-            editor.set_members(
-                vec![
-                    ChannelMembership {
-                        user: Arc::new(User {
-                            github_login: "a-b".into(),
-                            id: 101,
-                            avatar_uri: "avatar_a-b".into(),
-                        }),
-                        kind: proto::channel_member::Kind::Member,
-                        role: proto::ChannelRole::Member,
-                    },
-                    ChannelMembership {
-                        user: Arc::new(User {
-                            github_login: "C_D".into(),
-                            id: 102,
-                            avatar_uri: "avatar_C_D".into(),
-                        }),
-                        kind: proto::channel_member::Kind::Member,
-                        role: proto::ChannelRole::Member,
-                    },
-                ],
-                cx,
-            );
-
-            editor.editor.update(cx, |editor, cx| {
-                editor.set_text("Hello, @a-b! Have you met @C_D?", cx)
-            });
-        });
-
-        cx.executor().advance_clock(MENTIONS_DEBOUNCE_INTERVAL);
-
-        editor.update(cx, |editor, cx| {
-            let (text, ranges) = marked_text_ranges("Hello, «@a-b»! Have you met «@C_D»?", false);
-            assert_eq!(
-                editor.take_message(cx),
-                MessageParams {
-                    text,
-                    mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)],
-                }
-            );
-        });
-    }
-
-    fn init_test(cx: &mut TestAppContext) -> Arc<LanguageRegistry> {
-        cx.update(|cx| {
-            let http = FakeHttpClient::with_404_response();
-            let client = Client::new(http.clone(), cx);
-            let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
-            let settings = SettingsStore::test(cx);
-            cx.set_global(settings);
-            theme::init(theme::LoadThemes::JustBase, cx);
-            language::init(cx);
-            editor::init(cx);
-            client::init(&client, cx);
-            channel::init(&client, user_store, cx);
-        });
-
-        let language_registry = Arc::new(LanguageRegistry::test());
-        language_registry.add(Arc::new(Language::new(
-            LanguageConfig {
-                name: "Markdown".into(),
-                ..Default::default()
-            },
-            Some(tree_sitter_markdown::language()),
-        )));
-        language_registry
-    }
-}

crates/collab_ui2/src/collab_panel.rs 🔗

@@ -1,2539 +0,0 @@
-mod channel_modal;
-mod contact_finder;
-
-use self::channel_modal::ChannelModal;
-use crate::{
-    channel_view::ChannelView, chat_panel::ChatPanel, face_pile::FacePile,
-    CollaborationPanelSettings,
-};
-use call::ActiveCall;
-use channel::{Channel, ChannelEvent, ChannelId, ChannelStore};
-use client::{Client, Contact, User, UserStore};
-use contact_finder::ContactFinder;
-use db::kvp::KEY_VALUE_STORE;
-use editor::Editor;
-use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
-use fuzzy::{match_strings, StringMatchCandidate};
-use gpui::{
-    actions, canvas, div, fill, list, overlay, point, prelude::*, px, serde_json, AnyElement,
-    AppContext, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div, EventEmitter,
-    FocusHandle, FocusableView, InteractiveElement, IntoElement, ListOffset, ListState, Model,
-    MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, RenderOnce, SharedString,
-    Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView,
-};
-use menu::{Cancel, Confirm, SelectNext, SelectPrev};
-use project::{Fs, Project};
-use rpc::proto::{self, PeerId};
-use serde_derive::{Deserialize, Serialize};
-use settings::{Settings, SettingsStore};
-use smallvec::SmallVec;
-use std::{mem, sync::Arc};
-use theme::{ActiveTheme, ThemeSettings};
-use ui::prelude::*;
-use ui::{
-    h_stack, v_stack, Avatar, Button, Color, ContextMenu, Icon, IconButton, IconElement, IconSize,
-    Label, ListHeader, ListItem, Tooltip,
-};
-use util::{maybe, ResultExt, TryFutureExt};
-use workspace::{
-    dock::{DockPosition, Panel, PanelEvent},
-    notifications::NotifyResultExt,
-    Workspace,
-};
-
-actions!(
-    collab_panel,
-    [
-        ToggleFocus,
-        Remove,
-        Secondary,
-        CollapseSelectedChannel,
-        ExpandSelectedChannel,
-        StartMoveChannel,
-        MoveSelected,
-        InsertSpace,
-    ]
-);
-
-#[derive(Debug, Copy, Clone, PartialEq, Eq)]
-struct ChannelMoveClipboard {
-    channel_id: ChannelId,
-}
-
-const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel";
-
-pub fn init(cx: &mut AppContext) {
-    cx.observe_new_views(|workspace: &mut Workspace, _| {
-        workspace.register_action(|workspace, _: &ToggleFocus, cx| {
-            workspace.toggle_panel_focus::<CollabPanel>(cx);
-        });
-    })
-    .detach();
-}
-
-#[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<String> {
-        match self {
-            ChannelEditingState::Create { pending_name, .. } => pending_name.clone(),
-            ChannelEditingState::Rename { pending_name, .. } => pending_name.clone(),
-        }
-    }
-}
-
-pub struct CollabPanel {
-    width: Option<Pixels>,
-    fs: Arc<dyn Fs>,
-    focus_handle: FocusHandle,
-    channel_clipboard: Option<ChannelMoveClipboard>,
-    pending_serialization: Task<Option<()>>,
-    context_menu: Option<(View<ContextMenu>, Point<Pixels>, Subscription)>,
-    list_state: ListState,
-    filter_editor: View<Editor>,
-    channel_name_editor: View<Editor>,
-    channel_editing_state: Option<ChannelEditingState>,
-    entries: Vec<ListEntry>,
-    selection: Option<usize>,
-    channel_store: Model<ChannelStore>,
-    user_store: Model<UserStore>,
-    client: Arc<Client>,
-    project: Model<Project>,
-    match_candidates: Vec<StringMatchCandidate>,
-    subscriptions: Vec<Subscription>,
-    collapsed_sections: Vec<Section>,
-    collapsed_channels: Vec<ChannelId>,
-    workspace: WeakView<Workspace>,
-}
-
-#[derive(Serialize, Deserialize)]
-struct SerializedCollabPanel {
-    width: Option<Pixels>,
-    collapsed_channels: Option<Vec<u64>>,
-}
-
-#[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,
-}
-
-impl CollabPanel {
-    pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
-        cx.new_view(|cx| {
-            let filter_editor = cx.new_view(|cx| {
-                let mut editor = Editor::single_line(cx);
-                editor.set_placeholder_text("Filter...", cx);
-                editor
-            });
-
-            cx.subscribe(&filter_editor, |this: &mut Self, _, event, cx| {
-                if let editor::EditorEvent::BufferEdited = event {
-                    let query = this.filter_editor.read(cx).text(cx);
-                    if !query.is_empty() {
-                        this.selection.take();
-                    }
-                    this.update_entries(true, cx);
-                    if !query.is_empty() {
-                        this.selection = this
-                            .entries
-                            .iter()
-                            .position(|entry| !matches!(entry, ListEntry::Header(_)));
-                    }
-                }
-            })
-            .detach();
-
-            let channel_name_editor = cx.new_view(|cx| Editor::single_line(cx));
-
-            cx.subscribe(&channel_name_editor, |this: &mut Self, _, event, cx| {
-                if let editor::EditorEvent::Blurred = event {
-                    if let Some(state) = &this.channel_editing_state {
-                        if state.pending_name().is_some() {
-                            return;
-                        }
-                    }
-                    this.take_editing_state(cx);
-                    this.update_entries(false, cx);
-                    cx.notify();
-                }
-            })
-            .detach();
-
-            let view = cx.view().downgrade();
-            let list_state =
-                ListState::new(0, gpui::ListAlignment::Top, px(1000.), move |ix, cx| {
-                    if let Some(view) = view.upgrade() {
-                        view.update(cx, |view, cx| view.render_list_entry(ix, cx))
-                    } else {
-                        div().into_any()
-                    }
-                });
-
-            let mut this = Self {
-                width: None,
-                focus_handle: cx.focus_handle(),
-                channel_clipboard: None,
-                fs: workspace.app_state().fs.clone(),
-                pending_serialization: Task::ready(None),
-                context_menu: None,
-                list_state,
-                channel_name_editor,
-                filter_editor,
-                entries: Vec::default(),
-                channel_editing_state: None,
-                selection: None,
-                channel_store: ChannelStore::global(cx),
-                user_store: workspace.user_store().clone(),
-                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(),
-            };
-
-            this.update_entries(false, cx);
-
-            // Update the dock position when the setting changes.
-            let mut old_dock_position = this.position(cx);
-            this.subscriptions.push(cx.observe_global::<SettingsStore>(
-                move |this: &mut Self, cx| {
-                    let new_dock_position = this.position(cx);
-                    if new_dock_position != old_dock_position {
-                        old_dock_position = new_dock_position;
-                        cx.emit(PanelEvent::ChangePosition);
-                    }
-                    cx.notify();
-                },
-            ));
-
-            let active_call = ActiveCall::global(cx);
-            this.subscriptions
-                .push(cx.observe(&this.user_store, |this, _, cx| {
-                    this.update_entries(true, cx)
-                }));
-            this.subscriptions
-                .push(cx.observe(&this.channel_store, |this, _, cx| {
-                    this.update_entries(true, cx)
-                }));
-            this.subscriptions
-                .push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx)));
-            this.subscriptions
-                .push(cx.observe_flag::<ChannelsAlpha, _>(move |_, this, cx| {
-                    this.update_entries(true, cx)
-                }));
-            this.subscriptions.push(cx.subscribe(
-                &this.channel_store,
-                |this, _channel_store, e, cx| match e {
-                    ChannelEvent::ChannelCreated(channel_id)
-                    | ChannelEvent::ChannelRenamed(channel_id) => {
-                        if this.take_editing_state(cx) {
-                            this.update_entries(false, cx);
-                            this.selection = this.entries.iter().position(|entry| {
-                                if let ListEntry::Channel { channel, .. } = entry {
-                                    channel.id == *channel_id
-                                } else {
-                                    false
-                                }
-                            });
-                        }
-                    }
-                },
-            ));
-
-            this
-        })
-    }
-
-    pub async fn load(
-        workspace: WeakView<Workspace>,
-        mut cx: AsyncWindowContext,
-    ) -> anyhow::Result<View<Self>> {
-        let serialized_panel = cx
-            .background_executor()
-            .spawn(async move { KEY_VALUE_STORE.read_kvp(COLLABORATION_PANEL_KEY) })
-            .await
-            .map_err(|_| anyhow::anyhow!("Failed to read collaboration panel from key value store"))
-            .log_err()
-            .flatten()
-            .map(|panel| serde_json::from_str::<SerializedCollabPanel>(&panel))
-            .transpose()
-            .log_err()
-            .flatten();
-
-        workspace.update(&mut cx, |workspace, cx| {
-            let panel = CollabPanel::new(workspace, cx);
-            if let Some(serialized_panel) = serialized_panel {
-                panel.update(cx, |panel, cx| {
-                    panel.width = serialized_panel.width;
-                    panel.collapsed_channels = serialized_panel
-                        .collapsed_channels
-                        .unwrap_or_else(|| Vec::new());
-                    cx.notify();
-                });
-            }
-            panel
-        })
-    }
-
-    fn serialize(&mut self, cx: &mut ViewContext<Self>) {
-        let width = self.width;
-        let collapsed_channels = self.collapsed_channels.clone();
-        self.pending_serialization = cx.background_executor().spawn(
-            async move {
-                KEY_VALUE_STORE
-                    .write_kvp(
-                        COLLABORATION_PANEL_KEY.into(),
-                        serde_json::to_string(&SerializedCollabPanel {
-                            width,
-                            collapsed_channels: Some(collapsed_channels),
-                        })?,
-                    )
-                    .await?;
-                anyhow::Ok(())
-            }
-            .log_err(),
-        );
-    }
-
-    fn scroll_to_item(&mut self, ix: usize) {
-        self.list_state.scroll_to_reveal_item(ix)
-    }
-
-    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().into(),
-                            char_bag: channel.name.chars().collect(),
-                        },
-                    ));
-                let matches = executor.block(match_strings(
-                    &self.match_candidates,
-                    &query,
-                    true,
-                    usize::MAX,
-                    &Default::default(),
-                    executor.clone(),
-                ));
-                if let Some(state) = &self.channel_editing_state {
-                    if matches!(state, ChannelEditingState::Create { location: None, .. }) {
-                        self.entries.push(ListEntry::ChannelEditor { depth: 0 });
-                    }
-                }
-                let 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().into(),
-                            char_bag: channel.name.chars().collect(),
-                        }
-                    }));
-                let matches = executor.block(match_strings(
-                    &self.match_candidates,
-                    &query,
-                    true,
-                    usize::MAX,
-                    &Default::default(),
-                    executor.clone(),
-                ));
-                request_entries.extend(matches.iter().map(|mat| {
-                    ListEntry::ChannelInvite(channel_invites[mat.candidate_id].clone())
-                }));
-
-                if !request_entries.is_empty() {
-                    self.entries
-                        .push(ListEntry::Header(Section::ChannelInvites));
-                    if !self.collapsed_sections.contains(&Section::ChannelInvites) {
-                        self.entries.append(&mut request_entries);
-                    }
-                }
-            }
-        }
-
-        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: Pixels::ZERO,
-                        })
-                    })
-                    .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: Pixels::ZERO,
-                        })
-                    });
-
-                self.list_state
-                    .scroll_to(new_scroll_top.unwrap_or(old_scroll_top));
-            }
-        }
-
-        cx.notify();
-    }
-
-    fn render_call_participant(
-        &self,
-        user: &Arc<User>,
-        peer_id: Option<PeerId>,
-        is_pending: bool,
-        is_selected: bool,
-        cx: &mut ViewContext<Self>,
-    ) -> ListItem {
-        let is_current_user =
-            self.user_store.read(cx).current_user().map(|user| user.id) == Some(user.id);
-        let tooltip = format!("Follow {}", user.github_login);
-
-        ListItem::new(SharedString::from(user.github_login.clone()))
-            .start_slot(Avatar::new(user.avatar_uri.clone()))
-            .child(Label::new(user.github_login.clone()))
-            .selected(is_selected)
-            .end_slot(if is_pending {
-                Label::new("Calling").color(Color::Muted).into_any_element()
-            } else if is_current_user {
-                IconButton::new("leave-call", Icon::Exit)
-                    .style(ButtonStyle::Subtle)
-                    .on_click(move |_, cx| Self::leave_call(cx))
-                    .tooltip(|cx| Tooltip::text("Leave Call", cx))
-                    .into_any_element()
-            } else {
-                div().into_any_element()
-            })
-            .when_some(peer_id, |this, peer_id| {
-                this.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
-                    .on_click(cx.listener(move |this, _, cx| {
-                        this.workspace
-                            .update(cx, |workspace, cx| workspace.follow(peer_id, cx))
-                            .ok();
-                    }))
-            })
-    }
-
-    fn render_participant_project(
-        &self,
-        project_id: u64,
-        worktree_root_names: &[String],
-        host_user_id: u64,
-        is_last: bool,
-        is_selected: bool,
-        cx: &mut ViewContext<Self>,
-    ) -> impl IntoElement {
-        let project_name: SharedString = if worktree_root_names.is_empty() {
-            "untitled".to_string()
-        } else {
-            worktree_root_names.join(", ")
-        }
-        .into();
-
-        ListItem::new(project_id as usize)
-            .selected(is_selected)
-            .on_click(cx.listener(move |this, _, cx| {
-                this.workspace
-                    .update(cx, |workspace, cx| {
-                        let app_state = workspace.app_state().clone();
-                        workspace::join_remote_project(project_id, host_user_id, app_state, cx)
-                            .detach_and_log_err(cx);
-                    })
-                    .ok();
-            }))
-            .start_slot(
-                h_stack()
-                    .gap_1()
-                    .child(render_tree_branch(is_last, cx))
-                    .child(IconButton::new(0, Icon::Folder)),
-            )
-            .child(Label::new(project_name.clone()))
-            .tooltip(move |cx| Tooltip::text(format!("Open {}", project_name), cx))
-    }
-
-    fn render_participant_screen(
-        &self,
-        peer_id: Option<PeerId>,
-        is_last: bool,
-        is_selected: bool,
-        cx: &mut ViewContext<Self>,
-    ) -> impl IntoElement {
-        let id = peer_id.map_or(usize::MAX, |id| id.as_u64() as usize);
-
-        ListItem::new(("screen", id))
-            .selected(is_selected)
-            .start_slot(
-                h_stack()
-                    .gap_1()
-                    .child(render_tree_branch(is_last, cx))
-                    .child(IconButton::new(0, Icon::Screen)),
-            )
-            .child(Label::new("Screen"))
-            .when_some(peer_id, |this, _| {
-                this.on_click(cx.listener(move |this, _, cx| {
-                    this.workspace
-                        .update(cx, |workspace, cx| {
-                            workspace.open_shared_screen(peer_id.unwrap(), cx)
-                        })
-                        .ok();
-                }))
-                .tooltip(move |cx| Tooltip::text(format!("Open shared screen"), cx))
-            })
-    }
-
-    fn take_editing_state(&mut self, cx: &mut ViewContext<Self>) -> bool {
-        if let Some(_) = self.channel_editing_state.take() {
-            self.channel_name_editor.update(cx, |editor, cx| {
-                editor.set_text("", cx);
-            });
-            true
-        } else {
-            false
-        }
-    }
-
-    fn render_channel_notes(
-        &self,
-        channel_id: ChannelId,
-        is_selected: bool,
-        cx: &mut ViewContext<Self>,
-    ) -> impl IntoElement {
-        ListItem::new("channel-notes")
-            .selected(is_selected)
-            .on_click(cx.listener(move |this, _, cx| {
-                this.open_channel_notes(channel_id, cx);
-            }))
-            .start_slot(
-                h_stack()
-                    .gap_1()
-                    .child(render_tree_branch(false, cx))
-                    .child(IconButton::new(0, Icon::File)),
-            )
-            .child(div().h_7().w_full().child(Label::new("notes")))
-            .tooltip(move |cx| Tooltip::text("Open Channel Notes", cx))
-    }
-
-    fn render_channel_chat(
-        &self,
-        channel_id: ChannelId,
-        is_selected: bool,
-        cx: &mut ViewContext<Self>,
-    ) -> impl IntoElement {
-        ListItem::new("channel-chat")
-            .selected(is_selected)
-            .on_click(cx.listener(move |this, _, cx| {
-                this.join_channel_chat(channel_id, cx);
-            }))
-            .start_slot(
-                h_stack()
-                    .gap_1()
-                    .child(render_tree_branch(false, cx))
-                    .child(IconButton::new(0, Icon::MessageBubbles)),
-            )
-            .child(Label::new("chat"))
-            .tooltip(move |cx| Tooltip::text("Open Chat", cx))
-    }
-
-    fn has_subchannels(&self, ix: usize) -> bool {
-        self.entries.get(ix).map_or(false, |entry| {
-            if let ListEntry::Channel { has_children, .. } = entry {
-                *has_children
-            } else {
-                false
-            }
-        })
-    }
-
-    fn deploy_channel_context_menu(
-        &mut self,
-        position: Point<Pixels>,
-        channel_id: ChannelId,
-        ix: usize,
-        cx: &mut ViewContext<Self>,
-    ) {
-        let clipboard_channel_name = self.channel_clipboard.as_ref().and_then(|clipboard| {
-            self.channel_store
-                .read(cx)
-                .channel_for_id(clipboard.channel_id)
-                .map(|channel| channel.name.clone())
-        });
-        let this = cx.view().clone();
-
-        let context_menu = ContextMenu::build(cx, |mut context_menu, cx| {
-            if self.has_subchannels(ix) {
-                let expand_action_name = if self.is_channel_collapsed(channel_id) {
-                    "Expand Subchannels"
-                } else {
-                    "Collapse Subchannels"
-                };
-                context_menu = context_menu.entry(
-                    expand_action_name,
-                    None,
-                    cx.handler_for(&this, move |this, cx| {
-                        this.toggle_channel_collapsed(channel_id, cx)
-                    }),
-                );
-            }
-
-            context_menu = context_menu
-                .entry(
-                    "Open Notes",
-                    None,
-                    cx.handler_for(&this, move |this, cx| {
-                        this.open_channel_notes(channel_id, cx)
-                    }),
-                )
-                .entry(
-                    "Open Chat",
-                    None,
-                    cx.handler_for(&this, move |this, cx| {
-                        this.join_channel_chat(channel_id, cx)
-                    }),
-                )
-                .entry(
-                    "Copy Channel Link",
-                    None,
-                    cx.handler_for(&this, move |this, cx| {
-                        this.copy_channel_link(channel_id, cx)
-                    }),
-                );
-
-            if self.channel_store.read(cx).is_channel_admin(channel_id) {
-                context_menu = context_menu
-                    .separator()
-                    .entry(
-                        "New Subchannel",
-                        None,
-                        cx.handler_for(&this, move |this, cx| this.new_subchannel(channel_id, cx)),
-                    )
-                    .entry(
-                        "Rename",
-                        None,
-                        cx.handler_for(&this, move |this, cx| this.rename_channel(channel_id, cx)),
-                    )
-                    .entry(
-                        "Move this channel",
-                        None,
-                        cx.handler_for(&this, move |this, cx| {
-                            this.start_move_channel(channel_id, cx)
-                        }),
-                    );
-
-                if let Some(channel_name) = clipboard_channel_name {
-                    context_menu = context_menu.separator().entry(
-                        format!("Move '#{}' here", channel_name),
-                        None,
-                        cx.handler_for(&this, move |this, cx| {
-                            this.move_channel_on_clipboard(channel_id, cx)
-                        }),
-                    );
-                }
-
-                context_menu = context_menu
-                    .separator()
-                    .entry(
-                        "Invite Members",
-                        None,
-                        cx.handler_for(&this, move |this, cx| this.invite_members(channel_id, cx)),
-                    )
-                    .entry(
-                        "Manage Members",
-                        None,
-                        cx.handler_for(&this, move |this, cx| this.manage_members(channel_id, cx)),
-                    )
-                    .entry(
-                        "Delete",
-                        None,
-                        cx.handler_for(&this, move |this, cx| this.remove_channel(channel_id, cx)),
-                    );
-            }
-
-            context_menu
-        });
-
-        cx.focus_view(&context_menu);
-        let subscription =
-            cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
-                if this.context_menu.as_ref().is_some_and(|context_menu| {
-                    context_menu.0.focus_handle(cx).contains_focused(cx)
-                }) {
-                    cx.focus_self();
-                }
-                this.context_menu.take();
-                cx.notify();
-            });
-        self.context_menu = Some((context_menu, position, subscription));
-
-        cx.notify();
-    }
-
-    fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
-        if self.take_editing_state(cx) {
-            cx.focus_view(&self.filter_editor);
-        } else {
-            self.filter_editor.update(cx, |editor, cx| {
-                if editor.buffer().read(cx).len(cx) > 0 {
-                    editor.set_text("", cx);
-                }
-            });
-        }
-
-        self.update_entries(false, cx);
-    }
-
-    fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
-        let ix = self.selection.map_or(0, |ix| ix + 1);
-        if ix < self.entries.len() {
-            self.selection = Some(ix);
-        }
-
-        if let Some(ix) = self.selection {
-            self.scroll_to_item(ix)
-        }
-        cx.notify();
-    }
-
-    fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
-        let ix = self.selection.take().unwrap_or(0);
-        if ix > 0 {
-            self.selection = Some(ix - 1);
-        }
-
-        if let Some(ix) = self.selection {
-            self.scroll_to_item(ix)
-        }
-        cx.notify();
-    }
-
-    fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
-        if self.confirm_channel_edit(cx) {
-            return;
-        }
-
-        if let Some(selection) = self.selection {
-            if let Some(entry) = self.entries.get(selection) {
-                match entry {
-                    ListEntry::Header(section) => match section {
-                        Section::ActiveCall => Self::leave_call(cx),
-                        Section::Channels => self.new_root_channel(cx),
-                        Section::Contacts => self.toggle_contact_finder(cx),
-                        Section::ContactRequests
-                        | Section::Online
-                        | Section::Offline
-                        | Section::ChannelInvites => {
-                            self.toggle_section_expanded(*section, cx);
-                        }
-                    },
-                    ListEntry::Contact { contact, calling } => {
-                        if contact.online && !contact.busy && !calling {
-                            self.call(contact.user.id, cx);
-                        }
-                    }
-                    ListEntry::ParticipantProject {
-                        project_id,
-                        host_user_id,
-                        ..
-                    } => {
-                        if let Some(workspace) = self.workspace.upgrade() {
-                            let app_state = workspace.read(cx).app_state().clone();
-                            workspace::join_remote_project(
-                                *project_id,
-                                *host_user_id,
-                                app_state,
-                                cx,
-                            )
-                            .detach_and_log_err(cx);
-                        }
-                    }
-                    ListEntry::ParticipantScreen { peer_id, .. } => {
-                        let Some(peer_id) = peer_id else {
-                            return;
-                        };
-                        if let Some(workspace) = self.workspace.upgrade() {
-                            workspace.update(cx, |workspace, cx| {
-                                workspace.open_shared_screen(*peer_id, cx)
-                            });
-                        }
-                    }
-                    ListEntry::Channel { channel, .. } => {
-                        let is_active = maybe!({
-                            let call_channel = ActiveCall::global(cx)
-                                .read(cx)
-                                .room()?
-                                .read(cx)
-                                .channel_id()?;
-
-                            Some(call_channel == channel.id)
-                        })
-                        .unwrap_or(false);
-                        if is_active {
-                            self.open_channel_notes(channel.id, cx)
-                        } else {
-                            self.join_channel(channel.id, cx)
-                        }
-                    }
-                    ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx),
-                    ListEntry::CallParticipant { user, peer_id, .. } => {
-                        if Some(user) == self.user_store.read(cx).current_user().as_ref() {
-                            Self::leave_call(cx);
-                        } else if let Some(peer_id) = peer_id {
-                            self.workspace
-                                .update(cx, |workspace, cx| workspace.follow(*peer_id, cx))
-                                .ok();
-                        }
-                    }
-                    ListEntry::IncomingRequest(user) => {
-                        self.respond_to_contact_request(user.id, true, cx)
-                    }
-                    ListEntry::ChannelInvite(channel) => {
-                        self.respond_to_channel_invite(channel.id, true, cx)
-                    }
-                    ListEntry::ChannelNotes { channel_id } => {
-                        self.open_channel_notes(*channel_id, cx)
-                    }
-                    ListEntry::ChannelChat { channel_id } => {
-                        self.join_channel_chat(*channel_id, cx)
-                    }
-
-                    ListEntry::OutgoingRequest(_) => {}
-                    ListEntry::ChannelEditor { .. } => {}
-                }
-            }
-        }
-    }
-
-    fn insert_space(&mut self, _: &InsertSpace, cx: &mut ViewContext<Self>) {
-        if self.channel_editing_state.is_some() {
-            self.channel_name_editor.update(cx, |editor, cx| {
-                editor.insert(" ", cx);
-            });
-        }
-    }
-
-    fn confirm_channel_edit(&mut self, cx: &mut ViewContext<CollabPanel>) -> bool {
-        if let Some(editing_state) = &mut self.channel_editing_state {
-            match editing_state {
-                ChannelEditingState::Create {
-                    location,
-                    pending_name,
-                    ..
-                } => {
-                    if pending_name.is_some() {
-                        return false;
-                    }
-                    let channel_name = self.channel_name_editor.read(cx).text(cx);
-
-                    *pending_name = Some(channel_name.clone());
-
-                    self.channel_store
-                        .update(cx, |channel_store, cx| {
-                            channel_store.create_channel(&channel_name, *location, cx)
-                        })
-                        .detach();
-                    cx.notify();
-                }
-                ChannelEditingState::Rename {
-                    location,
-                    pending_name,
-                } => {
-                    if pending_name.is_some() {
-                        return false;
-                    }
-                    let channel_name = self.channel_name_editor.read(cx).text(cx);
-                    *pending_name = Some(channel_name.clone());
-
-                    self.channel_store
-                        .update(cx, |channel_store, cx| {
-                            channel_store.rename(*location, &channel_name, cx)
-                        })
-                        .detach();
-                    cx.notify();
-                }
-            }
-            cx.focus_self();
-            true
-        } else {
-            false
-        }
-    }
-
-    fn toggle_section_expanded(&mut self, section: Section, cx: &mut ViewContext<Self>) {
-        if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) {
-            self.collapsed_sections.remove(ix);
-        } else {
-            self.collapsed_sections.push(section);
-        }
-        self.update_entries(false, cx);
-    }
-
-    fn collapse_selected_channel(
-        &mut self,
-        _: &CollapseSelectedChannel,
-        cx: &mut ViewContext<Self>,
-    ) {
-        let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else {
-            return;
-        };
-
-        if self.is_channel_collapsed(channel_id) {
-            return;
-        }
-
-        self.toggle_channel_collapsed(channel_id, cx);
-    }
-
-    fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext<Self>) {
-        let Some(id) = self.selected_channel().map(|channel| channel.id) else {
-            return;
-        };
-
-        if !self.is_channel_collapsed(id) {
-            return;
-        }
-
-        self.toggle_channel_collapsed(id, cx)
-    }
-
-    //     fn toggle_channel_collapsed_action(
-    //         &mut self,
-    //         action: &ToggleCollapse,
-    //         cx: &mut ViewContext<Self>,
-    //     ) {
-    //         self.toggle_channel_collapsed(action.location, cx);
-    //     }
-
-    fn toggle_channel_collapsed<'a>(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
-        match self.collapsed_channels.binary_search(&channel_id) {
-            Ok(ix) => {
-                self.collapsed_channels.remove(ix);
-            }
-            Err(ix) => {
-                self.collapsed_channels.insert(ix, channel_id);
-            }
-        };
-        self.serialize(cx);
-        self.update_entries(true, cx);
-        cx.notify();
-        cx.focus_self();
-    }
-
-    fn is_channel_collapsed(&self, channel_id: ChannelId) -> bool {
-        self.collapsed_channels.binary_search(&channel_id).is_ok()
-    }
-
-    fn leave_call(cx: &mut WindowContext) {
-        ActiveCall::global(cx)
-            .update(cx, |call, cx| call.hang_up(cx))
-            .detach_and_log_err(cx);
-    }
-
-    fn toggle_contact_finder(&mut self, cx: &mut ViewContext<Self>) {
-        if let Some(workspace) = self.workspace.upgrade() {
-            workspace.update(cx, |workspace, cx| {
-                workspace.toggle_modal(cx, |cx| {
-                    let mut finder = ContactFinder::new(self.user_store.clone(), cx);
-                    finder.set_query(self.filter_editor.read(cx).text(cx), cx);
-                    finder
-                });
-            });
-        }
-    }
-
-    fn new_root_channel(&mut self, cx: &mut ViewContext<Self>) {
-        self.channel_editing_state = Some(ChannelEditingState::Create {
-            location: None,
-            pending_name: None,
-        });
-        self.update_entries(false, cx);
-        self.select_channel_editor();
-        cx.focus_view(&self.channel_name_editor);
-        cx.notify();
-    }
-
-    fn select_channel_editor(&mut self) {
-        self.selection = self.entries.iter().position(|entry| match entry {
-            ListEntry::ChannelEditor { .. } => true,
-            _ => false,
-        });
-    }
-
-    fn new_subchannel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
-        self.collapsed_channels
-            .retain(|channel| *channel != channel_id);
-        self.channel_editing_state = Some(ChannelEditingState::Create {
-            location: Some(channel_id),
-            pending_name: None,
-        });
-        self.update_entries(false, cx);
-        self.select_channel_editor();
-        cx.focus_view(&self.channel_name_editor);
-        cx.notify();
-    }
-
-    fn invite_members(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
-        self.show_channel_modal(channel_id, channel_modal::Mode::InviteMembers, cx);
-    }
-
-    fn manage_members(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
-        self.show_channel_modal(channel_id, channel_modal::Mode::ManageMembers, cx);
-    }
-
-    fn remove_selected_channel(&mut self, _: &Remove, cx: &mut ViewContext<Self>) {
-        if let Some(channel) = self.selected_channel() {
-            self.remove_channel(channel.id, cx)
-        }
-    }
-
-    fn rename_selected_channel(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
-        if let Some(channel) = self.selected_channel() {
-            self.rename_channel(channel.id, cx);
-        }
-    }
-
-    fn rename_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
-        let channel_store = self.channel_store.read(cx);
-        if !channel_store.is_channel_admin(channel_id) {
-            return;
-        }
-        if let Some(channel) = channel_store.channel_for_id(channel_id).cloned() {
-            self.channel_editing_state = Some(ChannelEditingState::Rename {
-                location: channel_id,
-                pending_name: None,
-            });
-            self.channel_name_editor.update(cx, |editor, cx| {
-                editor.set_text(channel.name.clone(), cx);
-                editor.select_all(&Default::default(), cx);
-            });
-            cx.focus_view(&self.channel_name_editor);
-            self.update_entries(false, cx);
-            self.select_channel_editor();
-        }
-    }
-
-    fn start_move_channel(&mut self, channel_id: ChannelId, _cx: &mut ViewContext<Self>) {
-        self.channel_clipboard = Some(ChannelMoveClipboard { channel_id });
-    }
-
-    fn start_move_selected_channel(&mut self, _: &StartMoveChannel, cx: &mut ViewContext<Self>) {
-        if let Some(channel) = self.selected_channel() {
-            self.start_move_channel(channel.id, cx);
-        }
-    }
-
-    fn move_channel_on_clipboard(
-        &mut self,
-        to_channel_id: ChannelId,
-        cx: &mut ViewContext<CollabPanel>,
-    ) {
-        if let Some(clipboard) = self.channel_clipboard.take() {
-            self.channel_store.update(cx, |channel_store, cx| {
-                channel_store
-                    .move_channel(clipboard.channel_id, Some(to_channel_id), cx)
-                    .detach_and_log_err(cx)
-            })
-        }
-    }
-
-    fn open_channel_notes(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
-        if let Some(workspace) = self.workspace.upgrade() {
-            ChannelView::open(channel_id, workspace, cx).detach();
-        }
-    }
-
-    fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext<Self>) {
-        let Some(channel) = self.selected_channel() else {
-            return;
-        };
-        let Some(bounds) = self
-            .selection
-            .and_then(|ix| self.list_state.bounds_for_item(ix))
-        else {
-            return;
-        };
-
-        self.deploy_channel_context_menu(bounds.center(), channel.id, self.selection.unwrap(), cx);
-        cx.stop_propagation();
-    }
-
-    fn selected_channel(&self) -> Option<&Arc<Channel>> {
-        self.selection
-            .and_then(|ix| self.entries.get(ix))
-            .and_then(|entry| match entry {
-                ListEntry::Channel { channel, .. } => Some(channel),
-                _ => None,
-            })
-    }
-
-    fn show_channel_modal(
-        &mut self,
-        channel_id: ChannelId,
-        mode: channel_modal::Mode,
-        cx: &mut ViewContext<Self>,
-    ) {
-        let workspace = self.workspace.clone();
-        let user_store = self.user_store.clone();
-        let channel_store = self.channel_store.clone();
-        let members = self.channel_store.update(cx, |channel_store, cx| {
-            channel_store.get_channel_member_details(channel_id, cx)
-        });
-
-        cx.spawn(|_, mut cx| async move {
-            let members = members.await?;
-            workspace.update(&mut cx, |workspace, cx| {
-                workspace.toggle_modal(cx, |cx| {
-                    ChannelModal::new(
-                        user_store.clone(),
-                        channel_store.clone(),
-                        channel_id,
-                        mode,
-                        members,
-                        cx,
-                    )
-                });
-            })
-        })
-        .detach();
-    }
-
-    fn remove_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
-        let channel_store = self.channel_store.clone();
-        if let Some(channel) = channel_store.read(cx).channel_for_id(channel_id) {
-            let prompt_message = format!(
-                "Are you sure you want to remove the channel \"{}\"?",
-                channel.name
-            );
-            let answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
-            cx.spawn(|this, mut cx| async move {
-                if answer.await? == 0 {
-                    channel_store
-                        .update(&mut cx, |channels, _| channels.remove_channel(channel_id))?
-                        .await
-                        .notify_async_err(&mut cx);
-                    this.update(&mut cx, |_, cx| cx.focus_self()).ok();
-                }
-                anyhow::Ok(())
-            })
-            .detach();
-        }
-    }
-
-    fn remove_contact(&mut self, user_id: u64, github_login: &str, cx: &mut ViewContext<Self>) {
-        let user_store = self.user_store.clone();
-        let prompt_message = format!(
-            "Are you sure you want to remove \"{}\" from your contacts?",
-            github_login
-        );
-        let answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
-        cx.spawn(|_, mut cx| async move {
-            if answer.await? == 0 {
-                user_store
-                    .update(&mut cx, |store, cx| store.remove_contact(user_id, cx))?
-                    .await
-                    .notify_async_err(&mut cx);
-            }
-            anyhow::Ok(())
-        })
-        .detach_and_log_err(cx);
-    }
-
-    fn respond_to_contact_request(
-        &mut self,
-        user_id: u64,
-        accept: bool,
-        cx: &mut ViewContext<Self>,
-    ) {
-        self.user_store
-            .update(cx, |store, cx| {
-                store.respond_to_contact_request(user_id, accept, cx)
-            })
-            .detach_and_log_err(cx);
-    }
-
-    fn respond_to_channel_invite(
-        &mut self,
-        channel_id: u64,
-        accept: bool,
-        cx: &mut ViewContext<Self>,
-    ) {
-        self.channel_store
-            .update(cx, |store, cx| {
-                store.respond_to_channel_invite(channel_id, accept, cx)
-            })
-            .detach();
-    }
-
-    fn call(&mut self, recipient_user_id: u64, cx: &mut ViewContext<Self>) {
-        ActiveCall::global(cx)
-            .update(cx, |call, cx| {
-                call.invite(recipient_user_id, Some(self.project.clone()), cx)
-            })
-            .detach_and_log_err(cx);
-    }
-
-    fn join_channel(&self, channel_id: u64, cx: &mut ViewContext<Self>) {
-        let Some(workspace) = self.workspace.upgrade() else {
-            return;
-        };
-        let Some(handle) = cx.window_handle().downcast::<Workspace>() else {
-            return;
-        };
-        workspace::join_channel(
-            channel_id,
-            workspace.read(cx).app_state().clone(),
-            Some(handle),
-            cx,
-        )
-        .detach_and_log_err(cx)
-    }
-
-    fn join_channel_chat(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
-        let Some(workspace) = self.workspace.upgrade() else {
-            return;
-        };
-        cx.window_context().defer(move |cx| {
-            workspace.update(cx, |workspace, cx| {
-                if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
-                    panel.update(cx, |panel, cx| {
-                        panel
-                            .select_channel(channel_id, None, cx)
-                            .detach_and_log_err(cx);
-                    });
-                }
-            });
-        });
-    }
-
-    fn copy_channel_link(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
-        let channel_store = self.channel_store.read(cx);
-        let Some(channel) = channel_store.channel_for_id(channel_id) else {
-            return;
-        };
-        let item = ClipboardItem::new(channel.link());
-        cx.write_to_clipboard(item)
-    }
-
-    fn render_signed_out(&mut self, cx: &mut ViewContext<Self>) -> Div {
-        let collab_blurb = "Work with your team in realtime with collaborative editing, voice, shared notes and more.";
-
-        v_stack()
-            .gap_6()
-            .p_4()
-            .child(Label::new(collab_blurb))
-            .child(
-                v_stack()
-                    .gap_2()
-                    .child(
-                        Button::new("sign_in", "Sign in")
-                            .icon_color(Color::Muted)
-                            .icon(Icon::Github)
-                            .icon_position(IconPosition::Start)
-                            .style(ButtonStyle::Filled)
-                            .full_width()
-                            .on_click(cx.listener(|this, _, cx| {
-                                let client = this.client.clone();
-                                cx.spawn(|_, mut cx| async move {
-                                    client
-                                        .authenticate_and_connect(true, &cx)
-                                        .await
-                                        .notify_async_err(&mut cx);
-                                })
-                                .detach()
-                            })),
-                    )
-                    .child(
-                        div().flex().w_full().items_center().child(
-                            Label::new("Sign in to enable collaboration.")
-                                .color(Color::Muted)
-                                .size(LabelSize::Small),
-                        ),
-                    ),
-            )
-    }
-
-    fn render_list_entry(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement {
-        let entry = &self.entries[ix];
-
-        let is_selected = self.selection == Some(ix);
-        match entry {
-            ListEntry::Header(section) => {
-                let is_collapsed = self.collapsed_sections.contains(section);
-                self.render_header(*section, is_selected, is_collapsed, cx)
-                    .into_any_element()
-            }
-            ListEntry::Contact { contact, calling } => self
-                .render_contact(contact, *calling, is_selected, cx)
-                .into_any_element(),
-            ListEntry::ContactPlaceholder => self
-                .render_contact_placeholder(is_selected, cx)
-                .into_any_element(),
-            ListEntry::IncomingRequest(user) => self
-                .render_contact_request(user, true, is_selected, cx)
-                .into_any_element(),
-            ListEntry::OutgoingRequest(user) => self
-                .render_contact_request(user, false, is_selected, cx)
-                .into_any_element(),
-            ListEntry::Channel {
-                channel,
-                depth,
-                has_children,
-            } => self
-                .render_channel(channel, *depth, *has_children, is_selected, ix, cx)
-                .into_any_element(),
-            ListEntry::ChannelEditor { depth } => {
-                self.render_channel_editor(*depth, cx).into_any_element()
-            }
-            ListEntry::ChannelInvite(channel) => self
-                .render_channel_invite(channel, is_selected, cx)
-                .into_any_element(),
-            ListEntry::CallParticipant {
-                user,
-                peer_id,
-                is_pending,
-            } => self
-                .render_call_participant(user, *peer_id, *is_pending, is_selected, cx)
-                .into_any_element(),
-            ListEntry::ParticipantProject {
-                project_id,
-                worktree_root_names,
-                host_user_id,
-                is_last,
-            } => self
-                .render_participant_project(
-                    *project_id,
-                    &worktree_root_names,
-                    *host_user_id,
-                    *is_last,
-                    is_selected,
-                    cx,
-                )
-                .into_any_element(),
-            ListEntry::ParticipantScreen { peer_id, is_last } => self
-                .render_participant_screen(*peer_id, *is_last, is_selected, cx)
-                .into_any_element(),
-            ListEntry::ChannelNotes { channel_id } => self
-                .render_channel_notes(*channel_id, is_selected, cx)
-                .into_any_element(),
-            ListEntry::ChannelChat { channel_id } => self
-                .render_channel_chat(*channel_id, is_selected, cx)
-                .into_any_element(),
-        }
-    }
-
-    fn render_signed_in(&mut self, cx: &mut ViewContext<Self>) -> Div {
-        v_stack()
-            .size_full()
-            .child(list(self.list_state.clone()).full())
-            .child(
-                v_stack().p_2().child(
-                    v_stack()
-                        .border_primary(cx)
-                        .border_t()
-                        .child(self.filter_editor.clone()),
-                ),
-            )
-    }
-
-    fn render_header(
-        &self,
-        section: Section,
-        is_selected: bool,
-        is_collapsed: bool,
-        cx: &ViewContext<Self>,
-    ) -> impl IntoElement {
-        let mut channel_link = None;
-        let mut channel_tooltip_text = None;
-        let mut channel_icon = None;
-        // let mut is_dragged_over = false;
-
-        let text = match section {
-            Section::ActiveCall => {
-                let channel_name = maybe!({
-                    let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?;
-
-                    let channel = self.channel_store.read(cx).channel_for_id(channel_id)?;
-
-                    channel_link = Some(channel.link());
-                    (channel_icon, channel_tooltip_text) = match channel.visibility {
-                        proto::ChannelVisibility::Public => {
-                            (Some("icons/public.svg"), Some("Copy public channel link."))
-                        }
-                        proto::ChannelVisibility::Members => {
-                            (Some("icons/hash.svg"), Some("Copy private channel link."))
-                        }
-                    };
-
-                    Some(channel.name.as_ref())
-                });
-
-                if let Some(name) = channel_name {
-                    SharedString::from(format!("{}", name))
-                } else {
-                    SharedString::from("Current Call")
-                }
-            }
-            Section::ContactRequests => SharedString::from("Requests"),
-            Section::Contacts => SharedString::from("Contacts"),
-            Section::Channels => SharedString::from("Channels"),
-            Section::ChannelInvites => SharedString::from("Invites"),
-            Section::Online => SharedString::from("Online"),
-            Section::Offline => SharedString::from("Offline"),
-        };
-
-        let button = match section {
-            Section::ActiveCall => channel_link.map(|channel_link| {
-                let channel_link_copy = channel_link.clone();
-                IconButton::new("channel-link", Icon::Copy)
-                    .icon_size(IconSize::Small)
-                    .size(ButtonSize::None)
-                    .visible_on_hover("section-header")
-                    .on_click(move |_, cx| {
-                        let item = ClipboardItem::new(channel_link_copy.clone());
-                        cx.write_to_clipboard(item)
-                    })
-                    .tooltip(|cx| Tooltip::text("Copy channel link", cx))
-                    .into_any_element()
-            }),
-            Section::Contacts => Some(
-                IconButton::new("add-contact", Icon::Plus)
-                    .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx)))
-                    .tooltip(|cx| Tooltip::text("Search for new contact", cx))
-                    .into_any_element(),
-            ),
-            Section::Channels => Some(
-                IconButton::new("add-channel", Icon::Plus)
-                    .on_click(cx.listener(|this, _, cx| this.new_root_channel(cx)))
-                    .tooltip(|cx| Tooltip::text("Create a channel", cx))
-                    .into_any_element(),
-            ),
-            _ => None,
-        };
-
-        let can_collapse = match section {
-            Section::ActiveCall | Section::Channels | Section::Contacts => false,
-            Section::ChannelInvites
-            | Section::ContactRequests
-            | Section::Online
-            | Section::Offline => true,
-        };
-
-        h_stack()
-            .w_full()
-            .group("section-header")
-            .child(
-                ListHeader::new(text)
-                    .when(can_collapse, |header| {
-                        header.toggle(Some(!is_collapsed)).on_toggle(cx.listener(
-                            move |this, _, cx| {
-                                this.toggle_section_expanded(section, cx);
-                            },
-                        ))
-                    })
-                    .inset(true)
-                    .end_slot::<AnyElement>(button)
-                    .selected(is_selected),
-            )
-            .when(section == Section::Channels, |el| {
-                el.drag_over::<Channel>(|style| style.bg(cx.theme().colors().ghost_element_hover))
-                    .on_drop(cx.listener(move |this, dragged_channel: &Channel, cx| {
-                        this.channel_store
-                            .update(cx, |channel_store, cx| {
-                                channel_store.move_channel(dragged_channel.id, None, cx)
-                            })
-                            .detach_and_log_err(cx)
-                    }))
-            })
-    }
-
-    fn render_contact(
-        &self,
-        contact: &Contact,
-        calling: bool,
-        is_selected: bool,
-        cx: &mut ViewContext<Self>,
-    ) -> impl IntoElement {
-        let online = contact.online;
-        let busy = contact.busy || calling;
-        let user_id = contact.user.id;
-        let github_login = SharedString::from(contact.user.github_login.clone());
-        let item =
-            ListItem::new(github_login.clone())
-                .indent_level(1)
-                .indent_step_size(px(20.))
-                .selected(is_selected)
-                .on_click(cx.listener(move |this, _, cx| this.call(user_id, cx)))
-                .child(
-                    h_stack()
-                        .w_full()
-                        .justify_between()
-                        .child(Label::new(github_login.clone()))
-                        .when(calling, |el| {
-                            el.child(Label::new("Calling").color(Color::Muted))
-                        })
-                        .when(!calling, |el| {
-                            el.child(
-                                IconButton::new("remove_contact", Icon::Close)
-                                    .icon_color(Color::Muted)
-                                    .visible_on_hover("")
-                                    .tooltip(|cx| Tooltip::text("Remove Contact", cx))
-                                    .on_click(cx.listener({
-                                        let github_login = github_login.clone();
-                                        move |this, _, cx| {
-                                            this.remove_contact(user_id, &github_login, cx);
-                                        }
-                                    })),
-                            )
-                        }),
-                )
-                .start_slot(
-                    // todo!() handle contacts with no avatar
-                    Avatar::new(contact.user.avatar_uri.clone())
-                        .availability_indicator(if online { Some(!busy) } else { None }),
-                )
-                .when(online && !busy, |el| {
-                    el.on_click(cx.listener(move |this, _, cx| this.call(user_id, cx)))
-                });
-
-        div()
-            .id(github_login.clone())
-            .group("")
-            .child(item)
-            .tooltip(move |cx| {
-                let text = if !online {
-                    format!(" {} is offline", &github_login)
-                } else if busy {
-                    format!(" {} is on a call", &github_login)
-                } else {
-                    let room = ActiveCall::global(cx).read(cx).room();
-                    if room.is_some() {
-                        format!("Invite {} to join call", &github_login)
-                    } else {
-                        format!("Call {}", &github_login)
-                    }
-                };
-                Tooltip::text(text, cx)
-            })
-    }
-
-    fn render_contact_request(
-        &self,
-        user: &Arc<User>,
-        is_incoming: bool,
-        is_selected: bool,
-        cx: &mut ViewContext<Self>,
-    ) -> impl IntoElement {
-        let github_login = SharedString::from(user.github_login.clone());
-        let user_id = user.id;
-        let is_response_pending = self.user_store.read(cx).is_contact_request_pending(&user);
-        let color = if is_response_pending {
-            Color::Muted
-        } else {
-            Color::Default
-        };
-
-        let controls = if is_incoming {
-            vec![
-                IconButton::new("decline-contact", Icon::Close)
-                    .on_click(cx.listener(move |this, _, cx| {
-                        this.respond_to_contact_request(user_id, false, cx);
-                    }))
-                    .icon_color(color)
-                    .tooltip(|cx| Tooltip::text("Decline invite", cx)),
-                IconButton::new("accept-contact", Icon::Check)
-                    .on_click(cx.listener(move |this, _, cx| {
-                        this.respond_to_contact_request(user_id, true, cx);
-                    }))
-                    .icon_color(color)
-                    .tooltip(|cx| Tooltip::text("Accept invite", cx)),
-            ]
-        } else {
-            let github_login = github_login.clone();
-            vec![IconButton::new("remove_contact", Icon::Close)
-                .on_click(cx.listener(move |this, _, cx| {
-                    this.remove_contact(user_id, &github_login, cx);
-                }))
-                .icon_color(color)
-                .tooltip(|cx| Tooltip::text("Cancel invite", cx))]
-        };
-
-        ListItem::new(github_login.clone())
-            .indent_level(1)
-            .indent_step_size(px(20.))
-            .selected(is_selected)
-            .child(
-                h_stack()
-                    .w_full()
-                    .justify_between()
-                    .child(Label::new(github_login.clone()))
-                    .child(h_stack().children(controls)),
-            )
-            .start_slot(Avatar::new(user.avatar_uri.clone()))
-    }
-
-    fn render_channel_invite(
-        &self,
-        channel: &Arc<Channel>,
-        is_selected: bool,
-        cx: &mut ViewContext<Self>,
-    ) -> ListItem {
-        let channel_id = channel.id;
-        let response_is_pending = self
-            .channel_store
-            .read(cx)
-            .has_pending_channel_invite_response(&channel);
-        let color = if response_is_pending {
-            Color::Muted
-        } else {
-            Color::Default
-        };
-
-        let controls = [
-            IconButton::new("reject-invite", Icon::Close)
-                .on_click(cx.listener(move |this, _, cx| {
-                    this.respond_to_channel_invite(channel_id, false, cx);
-                }))
-                .icon_color(color)
-                .tooltip(|cx| Tooltip::text("Decline invite", cx)),
-            IconButton::new("accept-invite", Icon::Check)
-                .on_click(cx.listener(move |this, _, cx| {
-                    this.respond_to_channel_invite(channel_id, true, cx);
-                }))
-                .icon_color(color)
-                .tooltip(|cx| Tooltip::text("Accept invite", cx)),
-        ];
-
-        ListItem::new(("channel-invite", channel.id as usize))
-            .selected(is_selected)
-            .child(
-                h_stack()
-                    .w_full()
-                    .justify_between()
-                    .child(Label::new(channel.name.clone()))
-                    .child(h_stack().children(controls)),
-            )
-            .start_slot(
-                IconElement::new(Icon::Hash)
-                    .size(IconSize::Small)
-                    .color(Color::Muted),
-            )
-    }
-
-    fn render_contact_placeholder(
-        &self,
-        is_selected: bool,
-        cx: &mut ViewContext<Self>,
-    ) -> ListItem {
-        ListItem::new("contact-placeholder")
-            .child(IconElement::new(Icon::Plus))
-            .child(Label::new("Add a Contact"))
-            .selected(is_selected)
-            .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx)))
-    }
-
-    fn render_channel(
-        &self,
-        channel: &Channel,
-        depth: usize,
-        has_children: bool,
-        is_selected: bool,
-        ix: usize,
-        cx: &mut ViewContext<Self>,
-    ) -> impl IntoElement {
-        let channel_id = channel.id;
-
-        let is_active = maybe!({
-            let call_channel = ActiveCall::global(cx)
-                .read(cx)
-                .room()?
-                .read(cx)
-                .channel_id()?;
-            Some(call_channel == channel_id)
-        })
-        .unwrap_or(false);
-        let is_public = self
-            .channel_store
-            .read(cx)
-            .channel_for_id(channel_id)
-            .map(|channel| channel.visibility)
-            == Some(proto::ChannelVisibility::Public);
-        let disclosed =
-            has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok());
-
-        let has_messages_notification = channel.unseen_message_id.is_some();
-        let has_notes_notification = channel.unseen_note_version.is_some();
-
-        const FACEPILE_LIMIT: usize = 3;
-        let participants = self.channel_store.read(cx).channel_participants(channel_id);
-
-        let face_pile = if !participants.is_empty() {
-            let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
-            let result = FacePile {
-                faces: participants
-                    .iter()
-                    .map(|user| Avatar::new(user.avatar_uri.clone()).into_any_element())
-                    .take(FACEPILE_LIMIT)
-                    .chain(if extra_count > 0 {
-                        // todo!() @nate - this label looks wrong.
-                        Some(Label::new(format!("+{}", extra_count)).into_any_element())
-                    } else {
-                        None
-                    })
-                    .collect::<SmallVec<_>>(),
-            };
-
-            Some(result)
-        } else {
-            None
-        };
-
-        let button_container = |cx: &mut ViewContext<Self>| {
-            h_stack()
-                .absolute()
-                // We're using a negative coordinate for the right anchor to
-                // counteract the padding of the `ListItem`.
-                //
-                // This prevents a gap from showing up between the background
-                // of this element and the edge of the collab panel.
-                .right(rems(-0.5))
-                // HACK: Without this the channel name clips on top of the icons, but I'm not sure why.
-                .z_index(10)
-                .bg(cx.theme().colors().panel_background)
-                .when(is_selected || is_active, |this| {
-                    this.bg(cx.theme().colors().ghost_element_selected)
-                })
-        };
-
-        let messages_button = |cx: &mut ViewContext<Self>| {
-            IconButton::new("channel_chat", Icon::MessageBubbles)
-                .icon_size(IconSize::Small)
-                .icon_color(if has_messages_notification {
-                    Color::Default
-                } else {
-                    Color::Muted
-                })
-                .on_click(cx.listener(move |this, _, cx| this.join_channel_chat(channel_id, cx)))
-                .tooltip(|cx| Tooltip::text("Open channel chat", cx))
-        };
-
-        let notes_button = |cx: &mut ViewContext<Self>| {
-            IconButton::new("channel_notes", Icon::File)
-                .icon_size(IconSize::Small)
-                .icon_color(if has_notes_notification {
-                    Color::Default
-                } else {
-                    Color::Muted
-                })
-                .on_click(cx.listener(move |this, _, cx| this.open_channel_notes(channel_id, cx)))
-                .tooltip(|cx| Tooltip::text("Open channel notes", cx))
-        };
-
-        let width = self.width.unwrap_or(px(240.));
-
-        div()
-            .id(channel_id as usize)
-            .group("")
-            .flex()
-            .w_full()
-            .on_drag(channel.clone(), move |channel, cx| {
-                cx.new_view(|_| DraggedChannelView {
-                    channel: channel.clone(),
-                    width,
-                })
-            })
-            .drag_over::<Channel>(|style| style.bg(cx.theme().colors().ghost_element_hover))
-            .on_drop(cx.listener(move |this, dragged_channel: &Channel, cx| {
-                this.channel_store
-                    .update(cx, |channel_store, cx| {
-                        channel_store.move_channel(dragged_channel.id, Some(channel_id), cx)
-                    })
-                    .detach_and_log_err(cx)
-            }))
-            .child(
-                ListItem::new(channel_id as usize)
-                    // Add one level of depth for the disclosure arrow.
-                    .indent_level(depth + 1)
-                    .indent_step_size(px(20.))
-                    .selected(is_selected || is_active)
-                    .toggle(disclosed)
-                    .on_toggle(
-                        cx.listener(move |this, _, cx| {
-                            this.toggle_channel_collapsed(channel_id, cx)
-                        }),
-                    )
-                    .on_click(cx.listener(move |this, _, cx| {
-                        if is_active {
-                            this.open_channel_notes(channel_id, cx)
-                        } else {
-                            this.join_channel(channel_id, cx)
-                        }
-                    }))
-                    .on_secondary_mouse_down(cx.listener(
-                        move |this, event: &MouseDownEvent, cx| {
-                            this.deploy_channel_context_menu(event.position, channel_id, ix, cx)
-                        },
-                    ))
-                    .start_slot(
-                        IconElement::new(if is_public { Icon::Public } else { Icon::Hash })
-                            .size(IconSize::Small)
-                            .color(Color::Muted),
-                    )
-                    .child(
-                        h_stack()
-                            .id(channel_id as usize)
-                            // HACK: This is a dirty hack to help with the positioning of the button container.
-                            //
-                            // We're using a pixel width for the elements but then allowing the contents to
-                            // overflow. This means that the label and facepile will be shown, but will not
-                            // push the button container off the edge of the panel.
-                            .w_px()
-                            .child(Label::new(channel.name.clone()))
-                            .children(face_pile.map(|face_pile| face_pile.render(cx))),
-                    )
-                    .end_slot::<Div>(
-                        // If we have a notification for either button, we want to show the corresponding
-                        // button(s) as indicators.
-                        if has_messages_notification || has_notes_notification {
-                            Some(
-                                button_container(cx).child(
-                                    h_stack()
-                                        .px_1()
-                                        .children(
-                                            // We only want to render the messages button if there are unseen messages.
-                                            // This way we don't take up any space that might overlap the channel name
-                                            // when there are no notifications.
-                                            has_messages_notification.then(|| messages_button(cx)),
-                                        )
-                                        .child(
-                                            // We always want the notes button to take up space to prevent layout
-                                            // shift when hovering over the channel.
-                                            // However, if there are is no notes notification we just show an empty slot.
-                                            notes_button(cx)
-                                                .when(!has_notes_notification, |this| {
-                                                    this.visible_on_hover("")
-                                                }),
-                                        ),
-                                ),
-                            )
-                        } else {
-                            None
-                        },
-                    )
-                    .end_hover_slot(
-                        // When we hover the channel entry we want to always show both buttons.
-                        button_container(cx).child(
-                            h_stack()
-                                .px_1()
-                                // The element hover background has a slight transparency to it, so we
-                                // need to apply it to the inner element so that it blends with the solid
-                                // background color of the absolutely-positioned element.
-                                .group_hover("", |style| {
-                                    style.bg(cx.theme().colors().ghost_element_hover)
-                                })
-                                .child(messages_button(cx))
-                                .child(notes_button(cx)),
-                        ),
-                    ),
-            )
-            .tooltip(|cx| Tooltip::text("Join channel", cx))
-    }
-
-    fn render_channel_editor(&self, depth: usize, _cx: &mut ViewContext<Self>) -> impl IntoElement {
-        let item = ListItem::new("channel-editor")
-            .inset(false)
-            // Add one level of depth for the disclosure arrow.
-            .indent_level(depth + 1)
-            .indent_step_size(px(20.))
-            .start_slot(
-                IconElement::new(Icon::Hash)
-                    .size(IconSize::Small)
-                    .color(Color::Muted),
-            );
-
-        if let Some(pending_name) = self
-            .channel_editing_state
-            .as_ref()
-            .and_then(|state| state.pending_name())
-        {
-            item.child(Label::new(pending_name))
-        } else {
-            item.child(
-                div()
-                    .w_full()
-                    .py_1() // todo!() @nate this is a px off at the default font size.
-                    .child(self.channel_name_editor.clone()),
-            )
-        }
-    }
-}
-
-fn render_tree_branch(is_last: bool, cx: &mut WindowContext) -> impl IntoElement {
-    let rem_size = cx.rem_size();
-    let line_height = cx.text_style().line_height_in_pixels(rem_size);
-    let width = rem_size * 1.5;
-    let thickness = px(2.);
-    let color = cx.theme().colors().text;
-
-    canvas(move |bounds, cx| {
-        let start_x = (bounds.left() + bounds.right() - thickness) / 2.;
-        let start_y = (bounds.top() + bounds.bottom() - thickness) / 2.;
-        let right = bounds.right();
-        let top = bounds.top();
-
-        cx.paint_quad(fill(
-            Bounds::from_corners(
-                point(start_x, top),
-                point(
-                    start_x + thickness,
-                    if is_last { start_y } else { bounds.bottom() },
-                ),
-            ),
-            color,
-        ));
-        cx.paint_quad(fill(
-            Bounds::from_corners(point(start_x, start_y), point(right, start_y + thickness)),
-            color,
-        ));
-    })
-    .w(width)
-    .h(line_height)
-}
-
-impl Render for CollabPanel {
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
-        v_stack()
-            .key_context("CollabPanel")
-            .on_action(cx.listener(CollabPanel::cancel))
-            .on_action(cx.listener(CollabPanel::select_next))
-            .on_action(cx.listener(CollabPanel::select_prev))
-            .on_action(cx.listener(CollabPanel::confirm))
-            .on_action(cx.listener(CollabPanel::insert_space))
-            .on_action(cx.listener(CollabPanel::remove_selected_channel))
-            .on_action(cx.listener(CollabPanel::show_inline_context_menu))
-            .on_action(cx.listener(CollabPanel::rename_selected_channel))
-            .on_action(cx.listener(CollabPanel::collapse_selected_channel))
-            .on_action(cx.listener(CollabPanel::expand_selected_channel))
-            .on_action(cx.listener(CollabPanel::start_move_selected_channel))
-            .track_focus(&self.focus_handle)
-            .size_full()
-            .child(if self.user_store.read(cx).current_user().is_none() {
-                self.render_signed_out(cx)
-            } else {
-                self.render_signed_in(cx)
-            })
-            .children(self.context_menu.as_ref().map(|(menu, position, _)| {
-                overlay()
-                    .position(*position)
-                    .anchor(gpui::AnchorCorner::TopLeft)
-                    .child(menu.clone())
-            }))
-    }
-}
-
-impl EventEmitter<PanelEvent> for CollabPanel {}
-
-impl Panel for CollabPanel {
-    fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
-        CollaborationPanelSettings::get_global(cx).dock
-    }
-
-    fn position_is_valid(&self, position: DockPosition) -> bool {
-        matches!(position, DockPosition::Left | DockPosition::Right)
-    }
-
-    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
-        settings::update_settings_file::<CollaborationPanelSettings>(
-            self.fs.clone(),
-            cx,
-            move |settings| settings.dock = Some(position),
-        );
-    }
-
-    fn size(&self, cx: &gpui::WindowContext) -> Pixels {
-        self.width
-            .unwrap_or_else(|| CollaborationPanelSettings::get_global(cx).default_width)
-    }
-
-    fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
-        self.width = size;
-        self.serialize(cx);
-        cx.notify();
-    }
-
-    fn icon(&self, cx: &gpui::WindowContext) -> Option<ui::Icon> {
-        CollaborationPanelSettings::get_global(cx)
-            .button
-            .then(|| ui::Icon::Collab)
-    }
-
-    fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
-        Some("Collab Panel")
-    }
-
-    fn toggle_action(&self) -> Box<dyn gpui::Action> {
-        Box::new(ToggleFocus)
-    }
-
-    fn persistent_name() -> &'static str {
-        "CollabPanel"
-    }
-}
-
-impl FocusableView for CollabPanel {
-    fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
-        self.filter_editor.focus_handle(cx).clone()
-    }
-}
-
-impl PartialEq for ListEntry {
-    fn eq(&self, other: &Self) -> bool {
-        match self {
-            ListEntry::Header(section_1) => {
-                if let ListEntry::Header(section_2) = other {
-                    return section_1 == section_2;
-                }
-            }
-            ListEntry::CallParticipant { user: user_1, .. } => {
-                if let ListEntry::CallParticipant { user: user_2, .. } = other {
-                    return user_1.id == user_2.id;
-                }
-            }
-            ListEntry::ParticipantProject {
-                project_id: project_id_1,
-                ..
-            } => {
-                if let ListEntry::ParticipantProject {
-                    project_id: project_id_2,
-                    ..
-                } = other
-                {
-                    return project_id_1 == project_id_2;
-                }
-            }
-            ListEntry::ParticipantScreen {
-                peer_id: peer_id_1, ..
-            } => {
-                if let ListEntry::ParticipantScreen {
-                    peer_id: peer_id_2, ..
-                } = other
-                {
-                    return peer_id_1 == peer_id_2;
-                }
-            }
-            ListEntry::Channel {
-                channel: channel_1, ..
-            } => {
-                if let ListEntry::Channel {
-                    channel: channel_2, ..
-                } = other
-                {
-                    return channel_1.id == channel_2.id;
-                }
-            }
-            ListEntry::ChannelNotes { channel_id } => {
-                if let ListEntry::ChannelNotes {
-                    channel_id: other_id,
-                } = other
-                {
-                    return channel_id == other_id;
-                }
-            }
-            ListEntry::ChannelChat { channel_id } => {
-                if let ListEntry::ChannelChat {
-                    channel_id: other_id,
-                } = other
-                {
-                    return channel_id == other_id;
-                }
-            }
-            ListEntry::ChannelInvite(channel_1) => {
-                if let ListEntry::ChannelInvite(channel_2) = other {
-                    return channel_1.id == channel_2.id;
-                }
-            }
-            ListEntry::IncomingRequest(user_1) => {
-                if let ListEntry::IncomingRequest(user_2) = other {
-                    return user_1.id == user_2.id;
-                }
-            }
-            ListEntry::OutgoingRequest(user_1) => {
-                if let ListEntry::OutgoingRequest(user_2) = other {
-                    return user_1.id == user_2.id;
-                }
-            }
-            ListEntry::Contact {
-                contact: contact_1, ..
-            } => {
-                if let ListEntry::Contact {
-                    contact: contact_2, ..
-                } = other
-                {
-                    return contact_1.user.id == contact_2.user.id;
-                }
-            }
-            ListEntry::ChannelEditor { depth } => {
-                if let ListEntry::ChannelEditor { depth: other_depth } = other {
-                    return depth == other_depth;
-                }
-            }
-            ListEntry::ContactPlaceholder => {
-                if let ListEntry::ContactPlaceholder = other {
-                    return true;
-                }
-            }
-        }
-        false
-    }
-}
-
-struct DraggedChannelView {
-    channel: Channel,
-    width: Pixels,
-}
-
-impl Render for DraggedChannelView {
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Element {
-        let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
-        h_stack()
-            .font(ui_font)
-            .bg(cx.theme().colors().background)
-            .w(self.width)
-            .p_1()
-            .gap_1()
-            .child(
-                IconElement::new(
-                    if self.channel.visibility == proto::ChannelVisibility::Public {
-                        Icon::Public
-                    } else {
-                        Icon::Hash
-                    },
-                )
-                .size(IconSize::Small)
-                .color(Color::Muted),
-            )
-            .child(Label::new(self.channel.name.clone()))
-    }
-}

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

@@ -1,575 +0,0 @@
-use channel::{ChannelId, ChannelMembership, ChannelStore};
-use client::{
-    proto::{self, ChannelRole, ChannelVisibility},
-    User, UserId, UserStore,
-};
-use fuzzy::{match_strings, StringMatchCandidate};
-use gpui::{
-    actions, div, overlay, AppContext, ClipboardItem, DismissEvent, EventEmitter, FocusableView,
-    Model, ParentElement, Render, Styled, Subscription, Task, View, ViewContext, VisualContext,
-    WeakView,
-};
-use picker::{Picker, PickerDelegate};
-use std::sync::Arc;
-use ui::{prelude::*, Avatar, Checkbox, ContextMenu, ListItem, ListItemSpacing};
-use util::TryFutureExt;
-use workspace::ModalView;
-
-actions!(
-    channel_modal,
-    [
-        SelectNextControl,
-        ToggleMode,
-        ToggleMemberAdmin,
-        RemoveMember
-    ]
-);
-
-pub struct ChannelModal {
-    picker: View<Picker<ChannelModalDelegate>>,
-    channel_store: Model<ChannelStore>,
-    channel_id: ChannelId,
-}
-
-impl ChannelModal {
-    pub fn new(
-        user_store: Model<UserStore>,
-        channel_store: Model<ChannelStore>,
-        channel_id: ChannelId,
-        mode: Mode,
-        members: Vec<ChannelMembership>,
-        cx: &mut ViewContext<Self>,
-    ) -> Self {
-        cx.observe(&channel_store, |_, _, cx| cx.notify()).detach();
-        let channel_modal = cx.view().downgrade();
-        let picker = cx.new_view(|cx| {
-            Picker::new(
-                ChannelModalDelegate {
-                    channel_modal,
-                    matching_users: Vec::new(),
-                    matching_member_indices: Vec::new(),
-                    selected_index: 0,
-                    user_store: user_store.clone(),
-                    channel_store: channel_store.clone(),
-                    channel_id,
-                    match_candidates: Vec::new(),
-                    context_menu: None,
-                    members,
-                    mode,
-                },
-                cx,
-            )
-            .modal(false)
-        });
-
-        Self {
-            picker,
-            channel_store,
-            channel_id,
-        }
-    }
-
-    fn toggle_mode(&mut self, _: &ToggleMode, cx: &mut ViewContext<Self>) {
-        let mode = match self.picker.read(cx).delegate.mode {
-            Mode::ManageMembers => Mode::InviteMembers,
-            Mode::InviteMembers => Mode::ManageMembers,
-        };
-        self.set_mode(mode, cx);
-    }
-
-    fn set_mode(&mut self, mode: Mode, cx: &mut ViewContext<Self>) {
-        let channel_store = self.channel_store.clone();
-        let channel_id = self.channel_id;
-        cx.spawn(|this, mut cx| async move {
-            if mode == Mode::ManageMembers {
-                let mut members = channel_store
-                    .update(&mut cx, |channel_store, cx| {
-                        channel_store.get_channel_member_details(channel_id, cx)
-                    })?
-                    .await?;
-
-                members.sort_by(|a, b| a.sort_key().cmp(&b.sort_key()));
-
-                this.update(&mut cx, |this, cx| {
-                    this.picker
-                        .update(cx, |picker, _| picker.delegate.members = members);
-                })?;
-            }
-
-            this.update(&mut cx, |this, cx| {
-                this.picker.update(cx, |picker, cx| {
-                    let delegate = &mut picker.delegate;
-                    delegate.mode = mode;
-                    delegate.selected_index = 0;
-                    picker.set_query("", cx);
-                    picker.update_matches(picker.query(cx), cx);
-                    cx.notify()
-                });
-                cx.notify()
-            })
-        })
-        .detach();
-    }
-
-    fn set_channel_visiblity(&mut self, selection: &Selection, cx: &mut ViewContext<Self>) {
-        self.channel_store.update(cx, |channel_store, cx| {
-            channel_store
-                .set_channel_visibility(
-                    self.channel_id,
-                    match selection {
-                        Selection::Unselected => ChannelVisibility::Members,
-                        Selection::Selected => ChannelVisibility::Public,
-                        Selection::Indeterminate => return,
-                    },
-                    cx,
-                )
-                .detach_and_log_err(cx)
-        });
-    }
-
-    fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
-        cx.emit(DismissEvent);
-    }
-}
-
-impl EventEmitter<DismissEvent> for ChannelModal {}
-impl ModalView for ChannelModal {}
-
-impl FocusableView for ChannelModal {
-    fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
-        self.picker.focus_handle(cx)
-    }
-}
-
-impl Render for ChannelModal {
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
-        let channel_store = self.channel_store.read(cx);
-        let Some(channel) = channel_store.channel_for_id(self.channel_id) else {
-            return div();
-        };
-        let channel_name = channel.name.clone();
-        let channel_id = channel.id;
-        let visibility = channel.visibility;
-        let mode = self.picker.read(cx).delegate.mode;
-
-        v_stack()
-            .key_context("ChannelModal")
-            .on_action(cx.listener(Self::toggle_mode))
-            .on_action(cx.listener(Self::dismiss))
-            .elevation_3(cx)
-            .w(rems(34.))
-            .child(
-                v_stack()
-                    .px_2()
-                    .py_1()
-                    .rounded_t(px(8.))
-                    .bg(cx.theme().colors().element_background)
-                    .child(IconElement::new(Icon::Hash).size(IconSize::Medium))
-                    .child(Label::new(channel_name))
-                    .child(
-                        h_stack()
-                            .w_full()
-                            .justify_between()
-                            .child(
-                                h_stack()
-                                    .gap_2()
-                                    .child(
-                                        Checkbox::new(
-                                            "is-public",
-                                            if visibility == ChannelVisibility::Public {
-                                                ui::Selection::Selected
-                                            } else {
-                                                ui::Selection::Unselected
-                                            },
-                                        )
-                                        .on_click(cx.listener(Self::set_channel_visiblity)),
-                                    )
-                                    .child(Label::new("Public")),
-                            )
-                            .children(if visibility == ChannelVisibility::Public {
-                                Some(Button::new("copy-link", "Copy Link").on_click(cx.listener(
-                                    move |this, _, cx| {
-                                        if let Some(channel) =
-                                            this.channel_store.read(cx).channel_for_id(channel_id)
-                                        {
-                                            let item = ClipboardItem::new(channel.link());
-                                            cx.write_to_clipboard(item);
-                                        }
-                                    },
-                                )))
-                            } else {
-                                None
-                            }),
-                    )
-                    .child(
-                        div()
-                            .w_full()
-                            .flex()
-                            .flex_row()
-                            .child(
-                                Button::new("manage-members", "Manage Members")
-                                    .selected(mode == Mode::ManageMembers)
-                                    .on_click(cx.listener(|this, _, cx| {
-                                        this.set_mode(Mode::ManageMembers, cx);
-                                    })),
-                            )
-                            .child(
-                                Button::new("invite-members", "Invite Members")
-                                    .selected(mode == Mode::InviteMembers)
-                                    .on_click(cx.listener(|this, _, cx| {
-                                        this.set_mode(Mode::InviteMembers, cx);
-                                    })),
-                            ),
-                    ),
-            )
-            .child(self.picker.clone())
-    }
-}
-
-#[derive(Copy, Clone, PartialEq)]
-pub enum Mode {
-    ManageMembers,
-    InviteMembers,
-}
-
-pub struct ChannelModalDelegate {
-    channel_modal: WeakView<ChannelModal>,
-    matching_users: Vec<Arc<User>>,
-    matching_member_indices: Vec<usize>,
-    user_store: Model<UserStore>,
-    channel_store: Model<ChannelStore>,
-    channel_id: ChannelId,
-    selected_index: usize,
-    mode: Mode,
-    match_candidates: Vec<StringMatchCandidate>,
-    members: Vec<ChannelMembership>,
-    context_menu: Option<(View<ContextMenu>, Subscription)>,
-}
-
-impl PickerDelegate for ChannelModalDelegate {
-    type ListItem = ListItem;
-
-    fn placeholder_text(&self) -> Arc<str> {
-        "Search collaborator by username...".into()
-    }
-
-    fn match_count(&self) -> usize {
-        match self.mode {
-            Mode::ManageMembers => self.matching_member_indices.len(),
-            Mode::InviteMembers => self.matching_users.len(),
-        }
-    }
-
-    fn selected_index(&self) -> usize {
-        self.selected_index
-    }
-
-    fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
-        self.selected_index = ix;
-    }
-
-    fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
-        match self.mode {
-            Mode::ManageMembers => {
-                self.match_candidates.clear();
-                self.match_candidates
-                    .extend(self.members.iter().enumerate().map(|(id, member)| {
-                        StringMatchCandidate {
-                            id,
-                            string: member.user.github_login.clone(),
-                            char_bag: member.user.github_login.chars().collect(),
-                        }
-                    }));
-
-                let matches = cx.background_executor().block(match_strings(
-                    &self.match_candidates,
-                    &query,
-                    true,
-                    usize::MAX,
-                    &Default::default(),
-                    cx.background_executor().clone(),
-                ));
-
-                cx.spawn(|picker, mut cx| async move {
-                    picker
-                        .update(&mut cx, |picker, cx| {
-                            let delegate = &mut picker.delegate;
-                            delegate.matching_member_indices.clear();
-                            delegate
-                                .matching_member_indices
-                                .extend(matches.into_iter().map(|m| m.candidate_id));
-                            cx.notify();
-                        })
-                        .ok();
-                })
-            }
-            Mode::InviteMembers => {
-                let search_users = self
-                    .user_store
-                    .update(cx, |store, cx| store.fuzzy_search_users(query, cx));
-                cx.spawn(|picker, mut cx| async move {
-                    async {
-                        let users = search_users.await?;
-                        picker.update(&mut cx, |picker, cx| {
-                            picker.delegate.matching_users = users;
-                            cx.notify();
-                        })?;
-                        anyhow::Ok(())
-                    }
-                    .log_err()
-                    .await;
-                })
-            }
-        }
-    }
-
-    fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
-        if let Some((selected_user, role)) = self.user_at_index(self.selected_index) {
-            match self.mode {
-                Mode::ManageMembers => {
-                    self.show_context_menu(selected_user, role.unwrap_or(ChannelRole::Member), cx)
-                }
-                Mode::InviteMembers => match self.member_status(selected_user.id, cx) {
-                    Some(proto::channel_member::Kind::Invitee) => {
-                        self.remove_member(selected_user.id, cx);
-                    }
-                    Some(proto::channel_member::Kind::AncestorMember) | None => {
-                        self.invite_member(selected_user, cx)
-                    }
-                    Some(proto::channel_member::Kind::Member) => {}
-                },
-            }
-        }
-    }
-
-    fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
-        if self.context_menu.is_none() {
-            self.channel_modal
-                .update(cx, |_, cx| {
-                    cx.emit(DismissEvent);
-                })
-                .ok();
-        }
-    }
-
-    fn render_match(
-        &self,
-        ix: usize,
-        selected: bool,
-        cx: &mut ViewContext<Picker<Self>>,
-    ) -> Option<Self::ListItem> {
-        let (user, role) = self.user_at_index(ix)?;
-        let request_status = self.member_status(user.id, cx);
-
-        Some(
-            ListItem::new(ix)
-                .inset(true)
-                .spacing(ListItemSpacing::Sparse)
-                .selected(selected)
-                .start_slot(Avatar::new(user.avatar_uri.clone()))
-                .child(Label::new(user.github_login.clone()))
-                .end_slot(h_stack().gap_2().map(|slot| {
-                    match self.mode {
-                        Mode::ManageMembers => slot
-                            .children(
-                                if request_status == Some(proto::channel_member::Kind::Invitee) {
-                                    Some(Label::new("Invited"))
-                                } else {
-                                    None
-                                },
-                            )
-                            .children(match role {
-                                Some(ChannelRole::Admin) => Some(Label::new("Admin")),
-                                Some(ChannelRole::Guest) => Some(Label::new("Guest")),
-                                _ => None,
-                            })
-                            .child(IconButton::new("ellipsis", Icon::Ellipsis))
-                            .children(
-                                if let (Some((menu, _)), true) = (&self.context_menu, selected) {
-                                    Some(
-                                        overlay()
-                                            .anchor(gpui::AnchorCorner::TopLeft)
-                                            .child(menu.clone()),
-                                    )
-                                } else {
-                                    None
-                                },
-                            ),
-                        Mode::InviteMembers => match request_status {
-                            Some(proto::channel_member::Kind::Invitee) => {
-                                slot.children(Some(Label::new("Invited")))
-                            }
-                            Some(proto::channel_member::Kind::Member) => {
-                                slot.children(Some(Label::new("Member")))
-                            }
-                            _ => slot,
-                        },
-                    }
-                })),
-        )
-    }
-}
-
-impl ChannelModalDelegate {
-    fn member_status(
-        &self,
-        user_id: UserId,
-        cx: &AppContext,
-    ) -> Option<proto::channel_member::Kind> {
-        self.members
-            .iter()
-            .find_map(|membership| (membership.user.id == user_id).then_some(membership.kind))
-            .or_else(|| {
-                self.channel_store
-                    .read(cx)
-                    .has_pending_channel_invite(self.channel_id, user_id)
-                    .then_some(proto::channel_member::Kind::Invitee)
-            })
-    }
-
-    fn user_at_index(&self, ix: usize) -> Option<(Arc<User>, Option<ChannelRole>)> {
-        match self.mode {
-            Mode::ManageMembers => self.matching_member_indices.get(ix).and_then(|ix| {
-                let channel_membership = self.members.get(*ix)?;
-                Some((
-                    channel_membership.user.clone(),
-                    Some(channel_membership.role),
-                ))
-            }),
-            Mode::InviteMembers => Some((self.matching_users.get(ix).cloned()?, None)),
-        }
-    }
-
-    fn set_user_role(
-        &mut self,
-        user_id: UserId,
-        new_role: ChannelRole,
-        cx: &mut ViewContext<Picker<Self>>,
-    ) -> Option<()> {
-        let update = self.channel_store.update(cx, |store, cx| {
-            store.set_member_role(self.channel_id, user_id, new_role, cx)
-        });
-        cx.spawn(|picker, mut cx| async move {
-            update.await?;
-            picker.update(&mut cx, |picker, cx| {
-                let this = &mut picker.delegate;
-                if let Some(member) = this.members.iter_mut().find(|m| m.user.id == user_id) {
-                    member.role = new_role;
-                }
-                cx.focus_self();
-                cx.notify();
-            })
-        })
-        .detach_and_log_err(cx);
-        Some(())
-    }
-
-    fn remove_member(&mut self, user_id: UserId, cx: &mut ViewContext<Picker<Self>>) -> Option<()> {
-        let update = self.channel_store.update(cx, |store, cx| {
-            store.remove_member(self.channel_id, user_id, cx)
-        });
-        cx.spawn(|picker, mut cx| async move {
-            update.await?;
-            picker.update(&mut cx, |picker, cx| {
-                let this = &mut picker.delegate;
-                if let Some(ix) = this.members.iter_mut().position(|m| m.user.id == user_id) {
-                    this.members.remove(ix);
-                    this.matching_member_indices.retain_mut(|member_ix| {
-                        if *member_ix == ix {
-                            return false;
-                        } else if *member_ix > ix {
-                            *member_ix -= 1;
-                        }
-                        true
-                    })
-                }
-
-                this.selected_index = this
-                    .selected_index
-                    .min(this.matching_member_indices.len().saturating_sub(1));
-
-                picker.focus(cx);
-                cx.notify();
-            })
-        })
-        .detach_and_log_err(cx);
-        Some(())
-    }
-
-    fn invite_member(&mut self, user: Arc<User>, cx: &mut ViewContext<Picker<Self>>) {
-        let invite_member = self.channel_store.update(cx, |store, cx| {
-            store.invite_member(self.channel_id, user.id, ChannelRole::Member, cx)
-        });
-
-        cx.spawn(|this, mut cx| async move {
-            invite_member.await?;
-
-            this.update(&mut cx, |this, cx| {
-                let new_member = ChannelMembership {
-                    user,
-                    kind: proto::channel_member::Kind::Invitee,
-                    role: ChannelRole::Member,
-                };
-                let members = &mut this.delegate.members;
-                match members.binary_search_by_key(&new_member.sort_key(), |k| k.sort_key()) {
-                    Ok(ix) | Err(ix) => members.insert(ix, new_member),
-                }
-
-                cx.notify();
-            })
-        })
-        .detach_and_log_err(cx);
-    }
-
-    fn show_context_menu(
-        &mut self,
-        user: Arc<User>,
-        role: ChannelRole,
-        cx: &mut ViewContext<Picker<Self>>,
-    ) {
-        let user_id = user.id;
-        let picker = cx.view().clone();
-        let context_menu = ContextMenu::build(cx, |mut menu, _cx| {
-            menu = menu.entry("Remove Member", None, {
-                let picker = picker.clone();
-                move |cx| {
-                    picker.update(cx, |picker, cx| {
-                        picker.delegate.remove_member(user_id, cx);
-                    })
-                }
-            });
-
-            let picker = picker.clone();
-            match role {
-                ChannelRole::Admin => {
-                    menu = menu.entry("Revoke Admin", None, move |cx| {
-                        picker.update(cx, |picker, cx| {
-                            picker
-                                .delegate
-                                .set_user_role(user_id, ChannelRole::Member, cx);
-                        })
-                    });
-                }
-                ChannelRole::Member => {
-                    menu = menu.entry("Make Admin", None, move |cx| {
-                        picker.update(cx, |picker, cx| {
-                            picker
-                                .delegate
-                                .set_user_role(user_id, ChannelRole::Admin, cx);
-                        })
-                    });
-                }
-                _ => {}
-            };
-
-            menu
-        });
-        cx.focus_view(&context_menu);
-        let subscription = cx.subscribe(&context_menu, |picker, _, _: &DismissEvent, cx| {
-            picker.delegate.context_menu = None;
-            picker.focus(cx);
-            cx.notify();
-        });
-        self.context_menu = Some((context_menu, subscription));
-    }
-}

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

@@ -1,163 +0,0 @@
-use client::{ContactRequestStatus, User, UserStore};
-use gpui::{
-    AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model, ParentElement as _,
-    Render, Styled, Task, View, ViewContext, VisualContext, WeakView,
-};
-use picker::{Picker, PickerDelegate};
-use std::sync::Arc;
-use theme::ActiveTheme as _;
-use ui::{prelude::*, Avatar, ListItem, ListItemSpacing};
-use util::{ResultExt as _, TryFutureExt};
-use workspace::ModalView;
-
-pub struct ContactFinder {
-    picker: View<Picker<ContactFinderDelegate>>,
-}
-
-impl ContactFinder {
-    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.new_view(|cx| Picker::new(delegate, cx).modal(false));
-
-        Self { picker }
-    }
-
-    pub fn set_query(&mut self, query: String, cx: &mut ViewContext<Self>) {
-        self.picker.update(cx, |picker, cx| {
-            picker.set_query(query, cx);
-        });
-    }
-}
-
-impl Render for ContactFinder {
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
-        v_stack()
-            .elevation_3(cx)
-            .child(
-                v_stack()
-                    .px_2()
-                    .py_1()
-                    .bg(cx.theme().colors().element_background)
-                    // HACK: Prevent the background color from overflowing the parent container.
-                    .rounded_t(px(8.))
-                    .child(Label::new("Contacts"))
-                    .child(h_stack().child(Label::new("Invite new contacts"))),
-            )
-            .child(self.picker.clone())
-            .w(rems(34.))
-    }
-}
-
-pub struct ContactFinderDelegate {
-    parent: WeakView<ContactFinder>,
-    potential_contacts: Arc<[Arc<User>]>,
-    user_store: Model<UserStore>,
-    selected_index: usize,
-}
-
-impl EventEmitter<DismissEvent> for ContactFinder {}
-impl ModalView for ContactFinder {}
-
-impl FocusableView for ContactFinder {
-    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
-        self.picker.focus_handle(cx)
-    }
-}
-
-impl PickerDelegate for ContactFinderDelegate {
-    type ListItem = ListItem;
-
-    fn match_count(&self) -> usize {
-        self.potential_contacts.len()
-    }
-
-    fn selected_index(&self) -> usize {
-        self.selected_index
-    }
-
-    fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
-        self.selected_index = ix;
-    }
-
-    fn 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
-            .update(cx, |store, cx| store.fuzzy_search_users(query, cx));
-
-        cx.spawn(|picker, mut cx| async move {
-            async {
-                let potential_contacts = search_users.await?;
-                picker.update(&mut cx, |picker, cx| {
-                    picker.delegate.potential_contacts = potential_contacts.into();
-                    cx.notify();
-                })?;
-                anyhow::Ok(())
-            }
-            .log_err()
-            .await;
-        })
-    }
-
-    fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
-        if let Some(user) = self.potential_contacts.get(self.selected_index) {
-            let user_store = self.user_store.read(cx);
-            match user_store.contact_request_status(user) {
-                ContactRequestStatus::None | ContactRequestStatus::RequestReceived => {
-                    self.user_store
-                        .update(cx, |store, cx| store.request_contact(user.id, cx))
-                        .detach();
-                }
-                ContactRequestStatus::RequestSent => {
-                    self.user_store
-                        .update(cx, |store, cx| store.remove_contact(user.id, cx))
-                        .detach();
-                }
-                _ => {}
-            }
-        }
-    }
-
-    fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
-        self.parent
-            .update(cx, |_, cx| cx.emit(DismissEvent))
-            .log_err();
-    }
-
-    fn render_match(
-        &self,
-        ix: usize,
-        selected: bool,
-        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);
-
-        let icon_path = match request_status {
-            ContactRequestStatus::None | ContactRequestStatus::RequestReceived => {
-                Some("icons/check.svg")
-            }
-            ContactRequestStatus::RequestSent => Some("icons/x.svg"),
-            ContactRequestStatus::RequestAccepted => None,
-        };
-        Some(
-            ListItem::new(ix)
-                .inset(true)
-                .spacing(ListItemSpacing::Sparse)
-                .selected(selected)
-                .start_slot(Avatar::new(user.avatar_uri.clone()))
-                .child(Label::new(user.github_login.clone()))
-                .end_slot::<IconElement>(
-                    icon_path.map(|icon_path| IconElement::from_path(icon_path)),
-                ),
-        )
-    }
-}

crates/collab_ui2/src/collab_titlebar_item.rs 🔗

@@ -1,586 +0,0 @@
-use crate::face_pile::FacePile;
-use auto_update::AutoUpdateStatus;
-use call::{ActiveCall, ParticipantLocation, Room};
-use client::{proto::PeerId, Client, ParticipantIndex, User, UserStore};
-use gpui::{
-    actions, canvas, div, point, px, rems, Action, AnyElement, AppContext, Element, Hsla,
-    InteractiveElement, IntoElement, Model, ParentElement, Path, Render,
-    StatefulInteractiveElement, Styled, Subscription, View, ViewContext, VisualContext, WeakView,
-    WindowBounds,
-};
-use project::{Project, RepositoryEntry};
-use recent_projects::RecentProjects;
-use std::sync::Arc;
-use theme::{ActiveTheme, PlayerColors};
-use ui::{
-    h_stack, popover_menu, prelude::*, Avatar, Button, ButtonLike, ButtonStyle, ContextMenu, Icon,
-    IconButton, IconElement, Tooltip,
-};
-use util::ResultExt;
-use vcs_menu::{build_branch_list, BranchList, OpenRecent as ToggleVcsMenu};
-use workspace::{notifications::NotifyResultExt, Workspace};
-
-const MAX_PROJECT_NAME_LENGTH: usize = 40;
-const MAX_BRANCH_NAME_LENGTH: usize = 40;
-
-actions!(
-    collab,
-    [
-        ShareProject,
-        UnshareProject,
-        ToggleUserMenu,
-        ToggleProjectMenu,
-        SwitchBranch
-    ]
-);
-
-pub fn init(cx: &mut AppContext) {
-    cx.observe_new_views(|workspace: &mut Workspace, cx| {
-        let titlebar_item = cx.new_view(|cx| CollabTitlebarItem::new(workspace, cx));
-        workspace.set_titlebar_item(titlebar_item.into(), cx)
-    })
-    .detach();
-    // cx.add_action(CollabTitlebarItem::share_project);
-    // cx.add_action(CollabTitlebarItem::unshare_project);
-    // cx.add_action(CollabTitlebarItem::toggle_user_menu);
-    // cx.add_action(CollabTitlebarItem::toggle_vcs_menu);
-    // cx.add_action(CollabTitlebarItem::toggle_project_menu);
-}
-
-pub struct CollabTitlebarItem {
-    project: Model<Project>,
-    user_store: Model<UserStore>,
-    client: Arc<Client>,
-    workspace: WeakView<Workspace>,
-    _subscriptions: Vec<Subscription>,
-}
-
-impl Render for CollabTitlebarItem {
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
-        let room = ActiveCall::global(cx).read(cx).room().cloned();
-        let current_user = self.user_store.read(cx).current_user();
-        let client = self.client.clone();
-        let project_id = self.project.read(cx).remote_id();
-
-        h_stack()
-            .id("titlebar")
-            .justify_between()
-            .w_full()
-            .h(rems(1.75))
-            // Set a non-scaling min-height here to ensure the titlebar is
-            // always at least the height of the traffic lights.
-            .min_h(px(32.))
-            .map(|this| {
-                if matches!(cx.window_bounds(), WindowBounds::Fullscreen) {
-                    this.pl_2()
-                } else {
-                    // Use pixels here instead of a rem-based size because the macOS traffic
-                    // lights are a static size, and don't scale with the rest of the UI.
-                    this.pl(px(80.))
-                }
-            })
-            .bg(cx.theme().colors().title_bar_background)
-            .on_click(|event, cx| {
-                if event.up.click_count == 2 {
-                    cx.zoom_window();
-                }
-            })
-            // left side
-            .child(
-                h_stack()
-                    .gap_1()
-                    .children(self.render_project_host(cx))
-                    .child(self.render_project_name(cx))
-                    .children(self.render_project_branch(cx))
-                    .when_some(
-                        current_user.clone().zip(client.peer_id()).zip(room.clone()),
-                        |this, ((current_user, peer_id), room)| {
-                            let player_colors = cx.theme().players();
-                            let room = room.read(cx);
-                            let mut remote_participants =
-                                room.remote_participants().values().collect::<Vec<_>>();
-                            remote_participants.sort_by_key(|p| p.participant_index.0);
-
-                            this.children(self.render_collaborator(
-                                &current_user,
-                                peer_id,
-                                true,
-                                room.is_speaking(),
-                                room.is_muted(cx),
-                                &room,
-                                project_id,
-                                &current_user,
-                            ))
-                            .children(
-                                remote_participants.iter().filter_map(|collaborator| {
-                                    let is_present = project_id.map_or(false, |project_id| {
-                                        collaborator.location
-                                            == ParticipantLocation::SharedProject { project_id }
-                                    });
-
-                                    let face_pile = self.render_collaborator(
-                                        &collaborator.user,
-                                        collaborator.peer_id,
-                                        is_present,
-                                        collaborator.speaking,
-                                        collaborator.muted,
-                                        &room,
-                                        project_id,
-                                        &current_user,
-                                    )?;
-
-                                    Some(
-                                        v_stack()
-                                            .id(("collaborator", collaborator.user.id))
-                                            .child(face_pile)
-                                            .child(render_color_ribbon(
-                                                collaborator.participant_index,
-                                                player_colors,
-                                            ))
-                                            .cursor_pointer()
-                                            .on_click({
-                                                let peer_id = collaborator.peer_id;
-                                                cx.listener(move |this, _, cx| {
-                                                    this.workspace
-                                                        .update(cx, |workspace, cx| {
-                                                            workspace.follow(peer_id, cx);
-                                                        })
-                                                        .ok();
-                                                })
-                                            })
-                                            .tooltip({
-                                                let login = collaborator.user.github_login.clone();
-                                                move |cx| {
-                                                    Tooltip::text(format!("Follow {login}"), cx)
-                                                }
-                                            }),
-                                    )
-                                }),
-                            )
-                        },
-                    ),
-            )
-            // right side
-            .child(
-                h_stack()
-                    .gap_1()
-                    .pr_1()
-                    .when_some(room, |this, room| {
-                        let room = room.read(cx);
-                        let project = self.project.read(cx);
-                        let is_local = project.is_local();
-                        let is_shared = is_local && project.is_shared();
-                        let is_muted = room.is_muted(cx);
-                        let is_deafened = room.is_deafened().unwrap_or(false);
-                        let is_screen_sharing = room.is_screen_sharing();
-
-                        this.when(is_local, |this| {
-                            this.child(
-                                Button::new(
-                                    "toggle_sharing",
-                                    if is_shared { "Unshare" } else { "Share" },
-                                )
-                                .style(ButtonStyle::Subtle)
-                                .label_size(LabelSize::Small)
-                                .on_click(cx.listener(
-                                    move |this, _, cx| {
-                                        if is_shared {
-                                            this.unshare_project(&Default::default(), cx);
-                                        } else {
-                                            this.share_project(&Default::default(), cx);
-                                        }
-                                    },
-                                )),
-                            )
-                        })
-                        .child(
-                            IconButton::new("leave-call", ui::Icon::Exit)
-                                .style(ButtonStyle::Subtle)
-                                .icon_size(IconSize::Small)
-                                .on_click(move |_, cx| {
-                                    ActiveCall::global(cx)
-                                        .update(cx, |call, cx| call.hang_up(cx))
-                                        .detach_and_log_err(cx);
-                                }),
-                        )
-                        .child(
-                            IconButton::new(
-                                "mute-microphone",
-                                if is_muted {
-                                    ui::Icon::MicMute
-                                } else {
-                                    ui::Icon::Mic
-                                },
-                            )
-                            .style(ButtonStyle::Subtle)
-                            .icon_size(IconSize::Small)
-                            .selected(is_muted)
-                            .on_click(move |_, cx| crate::toggle_mute(&Default::default(), cx)),
-                        )
-                        .child(
-                            IconButton::new(
-                                "mute-sound",
-                                if is_deafened {
-                                    ui::Icon::AudioOff
-                                } else {
-                                    ui::Icon::AudioOn
-                                },
-                            )
-                            .style(ButtonStyle::Subtle)
-                            .icon_size(IconSize::Small)
-                            .selected(is_deafened)
-                            .tooltip(move |cx| {
-                                Tooltip::with_meta("Deafen Audio", None, "Mic will be muted", cx)
-                            })
-                            .on_click(move |_, cx| crate::toggle_mute(&Default::default(), cx)),
-                        )
-                        .child(
-                            IconButton::new("screen-share", ui::Icon::Screen)
-                                .style(ButtonStyle::Subtle)
-                                .icon_size(IconSize::Small)
-                                .selected(is_screen_sharing)
-                                .on_click(move |_, cx| {
-                                    crate::toggle_screen_sharing(&Default::default(), cx)
-                                }),
-                        )
-                    })
-                    .map(|el| {
-                        let status = self.client.status();
-                        let status = &*status.borrow();
-                        if matches!(status, client::Status::Connected { .. }) {
-                            el.child(self.render_user_menu_button(cx))
-                        } else {
-                            el.children(self.render_connection_status(status, cx))
-                                .child(self.render_sign_in_button(cx))
-                                .child(self.render_user_menu_button(cx))
-                        }
-                    }),
-            )
-    }
-}
-
-fn render_color_ribbon(participant_index: ParticipantIndex, colors: &PlayerColors) -> gpui::Canvas {
-    let color = colors.color_for_participant(participant_index.0).cursor;
-    canvas(move |bounds, cx| {
-        let mut path = Path::new(bounds.lower_left());
-        let height = bounds.size.height;
-        path.curve_to(bounds.origin + point(height, px(0.)), bounds.origin);
-        path.line_to(bounds.upper_right() - point(height, px(0.)));
-        path.curve_to(bounds.lower_right(), bounds.upper_right());
-        path.line_to(bounds.lower_left());
-        cx.paint_path(path, color);
-    })
-    .h_1()
-    .w_full()
-}
-
-impl CollabTitlebarItem {
-    pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
-        let project = workspace.project().clone();
-        let user_store = workspace.app_state().user_store.clone();
-        let client = workspace.app_state().client.clone();
-        let active_call = ActiveCall::global(cx);
-        let mut subscriptions = Vec::new();
-        subscriptions.push(
-            cx.observe(&workspace.weak_handle().upgrade().unwrap(), |_, _, cx| {
-                cx.notify()
-            }),
-        );
-        subscriptions.push(cx.observe(&project, |_, _, cx| cx.notify()));
-        subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx)));
-        subscriptions.push(cx.observe_window_activation(Self::window_activation_changed));
-        subscriptions.push(cx.observe(&user_store, |_, _, cx| cx.notify()));
-
-        Self {
-            workspace: workspace.weak_handle(),
-            project,
-            user_store,
-            client,
-            _subscriptions: subscriptions,
-        }
-    }
-
-    // resolve if you are in a room -> render_project_owner
-    // render_project_owner -> resolve if you are in a room -> Option<foo>
-
-    pub fn render_project_host(&self, cx: &mut ViewContext<Self>) -> Option<impl Element> {
-        let host = self.project.read(cx).host()?;
-        let host = self.user_store.read(cx).get_cached_user(host.user_id)?;
-        let participant_index = self
-            .user_store
-            .read(cx)
-            .participant_indices()
-            .get(&host.id)?;
-        Some(
-            div().border().border_color(gpui::red()).child(
-                Button::new("project_owner_trigger", host.github_login.clone())
-                    .color(Color::Player(participant_index.0))
-                    .style(ButtonStyle::Subtle)
-                    .label_size(LabelSize::Small)
-                    .tooltip(move |cx| Tooltip::text("Toggle following", cx)),
-            ),
-        )
-    }
-
-    pub fn render_project_name(&self, cx: &mut ViewContext<Self>) -> impl Element {
-        let name = {
-            let mut names = self.project.read(cx).visible_worktrees(cx).map(|worktree| {
-                let worktree = worktree.read(cx);
-                worktree.root_name()
-            });
-
-            names.next().unwrap_or("")
-        };
-
-        let name = util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH);
-        let workspace = self.workspace.clone();
-        popover_menu("project_name_trigger")
-            .trigger(
-                Button::new("project_name_trigger", name)
-                    .style(ButtonStyle::Subtle)
-                    .label_size(LabelSize::Small)
-                    .tooltip(move |cx| Tooltip::text("Recent Projects", cx)),
-            )
-            .menu(move |cx| Some(Self::render_project_popover(workspace.clone(), cx)))
-    }
-
-    pub fn render_project_branch(&self, cx: &mut ViewContext<Self>) -> Option<impl Element> {
-        let entry = {
-            let mut names_and_branches =
-                self.project.read(cx).visible_worktrees(cx).map(|worktree| {
-                    let worktree = worktree.read(cx);
-                    worktree.root_git_entry()
-                });
-
-            names_and_branches.next().flatten()
-        };
-        let workspace = self.workspace.upgrade()?;
-        let branch_name = entry
-            .as_ref()
-            .and_then(RepositoryEntry::branch)
-            .map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH))?;
-        Some(
-            popover_menu("project_branch_trigger")
-                .trigger(
-                    Button::new("project_branch_trigger", branch_name)
-                        .color(Color::Muted)
-                        .style(ButtonStyle::Subtle)
-                        .label_size(LabelSize::Small)
-                        .tooltip(move |cx| {
-                            Tooltip::with_meta(
-                                "Recent Branches",
-                                Some(&ToggleVcsMenu),
-                                "Local branches only",
-                                cx,
-                            )
-                        }),
-                )
-                .menu(move |cx| Self::render_vcs_popover(workspace.clone(), cx)),
-        )
-    }
-
-    fn render_collaborator(
-        &self,
-        user: &Arc<User>,
-        peer_id: PeerId,
-        is_present: bool,
-        is_speaking: bool,
-        is_muted: bool,
-        room: &Room,
-        project_id: Option<u64>,
-        current_user: &Arc<User>,
-    ) -> Option<FacePile> {
-        let followers = project_id.map_or(&[] as &[_], |id| room.followers_for(peer_id, id));
-
-        let pile = FacePile::default()
-            .child(
-                Avatar::new(user.avatar_uri.clone())
-                    .grayscale(!is_present)
-                    .border_color(if is_speaking {
-                        gpui::blue()
-                    } else if is_muted {
-                        gpui::red()
-                    } else {
-                        Hsla::default()
-                    }),
-            )
-            .children(followers.iter().filter_map(|follower_peer_id| {
-                let follower = room
-                    .remote_participants()
-                    .values()
-                    .find_map(|p| (p.peer_id == *follower_peer_id).then_some(&p.user))
-                    .or_else(|| {
-                        (self.client.peer_id() == Some(*follower_peer_id)).then_some(current_user)
-                    })?
-                    .clone();
-
-                Some(Avatar::new(follower.avatar_uri.clone()))
-            }));
-
-        Some(pile)
-    }
-
-    fn window_activation_changed(&mut self, cx: &mut ViewContext<Self>) {
-        let project = if cx.is_window_active() {
-            Some(self.project.clone())
-        } else {
-            None
-        };
-        ActiveCall::global(cx)
-            .update(cx, |call, cx| call.set_location(project.as_ref(), cx))
-            .detach_and_log_err(cx);
-    }
-
-    fn active_call_changed(&mut self, cx: &mut ViewContext<Self>) {
-        cx.notify();
-    }
-
-    fn share_project(&mut self, _: &ShareProject, cx: &mut ViewContext<Self>) {
-        let active_call = ActiveCall::global(cx);
-        let project = self.project.clone();
-        active_call
-            .update(cx, |call, cx| call.share_project(project, cx))
-            .detach_and_log_err(cx);
-    }
-
-    fn unshare_project(&mut self, _: &UnshareProject, cx: &mut ViewContext<Self>) {
-        let active_call = ActiveCall::global(cx);
-        let project = self.project.clone();
-        active_call
-            .update(cx, |call, cx| call.unshare_project(project, cx))
-            .log_err();
-    }
-
-    pub fn render_vcs_popover(
-        workspace: View<Workspace>,
-        cx: &mut WindowContext<'_>,
-    ) -> Option<View<BranchList>> {
-        let view = build_branch_list(workspace, cx).log_err()?;
-        let focus_handle = view.focus_handle(cx);
-        cx.focus(&focus_handle);
-        Some(view)
-    }
-
-    pub fn render_project_popover(
-        workspace: WeakView<Workspace>,
-        cx: &mut WindowContext<'_>,
-    ) -> View<RecentProjects> {
-        let view = RecentProjects::open_popover(workspace, cx);
-
-        let focus_handle = view.focus_handle(cx);
-        cx.focus(&focus_handle);
-        view
-    }
-
-    fn render_connection_status(
-        &self,
-        status: &client::Status,
-        cx: &mut ViewContext<Self>,
-    ) -> Option<AnyElement> {
-        match status {
-            client::Status::ConnectionError
-            | client::Status::ConnectionLost
-            | client::Status::Reauthenticating { .. }
-            | client::Status::Reconnecting { .. }
-            | client::Status::ReconnectionError { .. } => Some(
-                div()
-                    .id("disconnected")
-                    .bg(gpui::red()) // todo!() @nate
-                    .child(IconElement::new(Icon::Disconnected))
-                    .tooltip(|cx| Tooltip::text("Disconnected", cx))
-                    .into_any_element(),
-            ),
-            client::Status::UpgradeRequired => {
-                let auto_updater = auto_update::AutoUpdater::get(cx);
-                let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) {
-                    Some(AutoUpdateStatus::Updated) => "Please restart Zed to Collaborate",
-                    Some(AutoUpdateStatus::Installing)
-                    | Some(AutoUpdateStatus::Downloading)
-                    | Some(AutoUpdateStatus::Checking) => "Updating...",
-                    Some(AutoUpdateStatus::Idle) | Some(AutoUpdateStatus::Errored) | None => {
-                        "Please update Zed to Collaborate"
-                    }
-                };
-
-                Some(
-                    div()
-                        .bg(gpui::red()) // todo!() @nate
-                        .child(Button::new("connection-status", label).on_click(|_, cx| {
-                            if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) {
-                                if auto_updater.read(cx).status() == AutoUpdateStatus::Updated {
-                                    workspace::restart(&Default::default(), cx);
-                                    return;
-                                }
-                            }
-                            auto_update::check(&Default::default(), cx);
-                        }))
-                        .into_any_element(),
-                )
-            }
-            _ => None,
-        }
-    }
-
-    pub fn render_sign_in_button(&mut self, _: &mut ViewContext<Self>) -> Button {
-        let client = self.client.clone();
-        Button::new("sign_in", "Sign in").on_click(move |_, cx| {
-            let client = client.clone();
-            cx.spawn(move |mut cx| async move {
-                client
-                    .authenticate_and_connect(true, &cx)
-                    .await
-                    .notify_async_err(&mut cx);
-            })
-            .detach();
-        })
-    }
-
-    pub fn render_user_menu_button(&mut self, cx: &mut ViewContext<Self>) -> impl Element {
-        if let Some(user) = self.user_store.read(cx).current_user() {
-            popover_menu("user-menu")
-                .menu(|cx| {
-                    ContextMenu::build(cx, |menu, _| {
-                        menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
-                            .action("Theme", theme_selector::Toggle.boxed_clone())
-                            .separator()
-                            .action("Share Feedback", feedback::GiveFeedback.boxed_clone())
-                            .action("Sign Out", client::SignOut.boxed_clone())
-                    })
-                    .into()
-                })
-                .trigger(
-                    ButtonLike::new("user-menu")
-                        .child(
-                            h_stack()
-                                .gap_0p5()
-                                .child(Avatar::new(user.avatar_uri.clone()))
-                                .child(IconElement::new(Icon::ChevronDown).color(Color::Muted)),
-                        )
-                        .style(ButtonStyle::Subtle)
-                        .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),
-                )
-                .anchor(gpui::AnchorCorner::TopRight)
-        } else {
-            popover_menu("user-menu")
-                .menu(|cx| {
-                    ContextMenu::build(cx, |menu, _| {
-                        menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
-                            .action("Theme", theme_selector::Toggle.boxed_clone())
-                            .separator()
-                            .action("Share Feedback", feedback::GiveFeedback.boxed_clone())
-                    })
-                    .into()
-                })
-                .trigger(
-                    ButtonLike::new("user-menu")
-                        .child(
-                            h_stack()
-                                .gap_0p5()
-                                .child(IconElement::new(Icon::ChevronDown).color(Color::Muted)),
-                        )
-                        .style(ButtonStyle::Subtle)
-                        .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),
-                )
-        }
-    }
-}

crates/collab_ui2/src/collab_ui.rs 🔗

@@ -1,167 +0,0 @@
-pub mod channel_view;
-pub mod chat_panel;
-pub mod collab_panel;
-mod collab_titlebar_item;
-mod face_pile;
-pub mod notification_panel;
-pub mod notifications;
-mod panel_settings;
-
-use std::{rc::Rc, sync::Arc};
-
-use call::{report_call_event_for_room, ActiveCall, Room};
-pub use collab_panel::CollabPanel;
-pub use collab_titlebar_item::CollabTitlebarItem;
-use feature_flags::{ChannelsAlpha, FeatureFlagAppExt};
-use gpui::{
-    actions, point, AppContext, GlobalPixels, Pixels, PlatformDisplay, Size, Task, WindowBounds,
-    WindowKind, WindowOptions,
-};
-pub use panel_settings::{
-    ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings,
-};
-use settings::Settings;
-use util::ResultExt;
-use workspace::AppState;
-
-actions!(
-    collab,
-    [ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall]
-);
-
-pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
-    CollaborationPanelSettings::register(cx);
-    ChatPanelSettings::register(cx);
-    NotificationPanelSettings::register(cx);
-
-    vcs_menu::init(cx);
-    collab_titlebar_item::init(cx);
-    collab_panel::init(cx);
-    channel_view::init(cx);
-    chat_panel::init(cx);
-    notification_panel::init(cx);
-    notifications::init(&app_state, cx);
-
-    // cx.add_global_action(toggle_screen_sharing);
-    // cx.add_global_action(toggle_mute);
-    // cx.add_global_action(toggle_deafen);
-}
-
-pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
-    let call = ActiveCall::global(cx).read(cx);
-    if let Some(room) = call.room().cloned() {
-        let client = call.client();
-        let toggle_screen_sharing = room.update(cx, |room, cx| {
-            if room.is_screen_sharing() {
-                report_call_event_for_room(
-                    "disable screen share",
-                    room.id(),
-                    room.channel_id(),
-                    &client,
-                    cx,
-                );
-                Task::ready(room.unshare_screen(cx))
-            } else {
-                report_call_event_for_room(
-                    "enable screen share",
-                    room.id(),
-                    room.channel_id(),
-                    &client,
-                    cx,
-                );
-                room.share_screen(cx)
-            }
-        });
-        toggle_screen_sharing.detach_and_log_err(cx);
-    }
-}
-
-pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
-    let call = ActiveCall::global(cx).read(cx);
-    if let Some(room) = call.room().cloned() {
-        let client = call.client();
-        room.update(cx, |room, cx| {
-            let operation = if room.is_muted(cx) {
-                "enable microphone"
-            } else {
-                "disable microphone"
-            };
-            report_call_event_for_room(operation, room.id(), room.channel_id(), &client, cx);
-
-            room.toggle_mute(cx)
-        })
-        .map(|task| task.detach_and_log_err(cx))
-        .log_err();
-    }
-}
-
-pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) {
-    if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
-        room.update(cx, Room::toggle_deafen)
-            .map(|task| task.detach_and_log_err(cx))
-            .log_err();
-    }
-}
-
-fn notification_window_options(
-    screen: Rc<dyn PlatformDisplay>,
-    window_size: Size<Pixels>,
-) -> WindowOptions {
-    let notification_margin_width = GlobalPixels::from(16.);
-    let notification_margin_height = GlobalPixels::from(-0.) - GlobalPixels::from(48.);
-
-    let screen_bounds = screen.bounds();
-    let size: Size<GlobalPixels> = window_size.into();
-
-    // todo!() use content bounds instead of screen.bounds and get rid of magics in point's 2nd argument.
-    let bounds = gpui::Bounds::<GlobalPixels> {
-        origin: screen_bounds.upper_right()
-            - point(
-                size.width + notification_margin_width,
-                notification_margin_height,
-            ),
-        size: window_size.into(),
-    };
-    WindowOptions {
-        bounds: WindowBounds::Fixed(bounds),
-        titlebar: None,
-        center: false,
-        focus: false,
-        show: true,
-        kind: WindowKind::PopUp,
-        is_movable: false,
-        display_id: Some(screen.id()),
-    }
-}
-
-// fn render_avatar<T: 'static>(
-//     avatar: Option<Arc<ImageData>>,
-//     avatar_style: &AvatarStyle,
-//     container: ContainerStyle,
-// ) -> AnyElement<T> {
-//     avatar
-//         .map(|avatar| {
-//             Image::from_data(avatar)
-//                 .with_style(avatar_style.image)
-//                 .aligned()
-//                 .contained()
-//                 .with_corner_radius(avatar_style.outer_corner_radius)
-//                 .constrained()
-//                 .with_width(avatar_style.outer_width)
-//                 .with_height(avatar_style.outer_width)
-//                 .into_any()
-//         })
-//         .unwrap_or_else(|| {
-//             Empty::new()
-//                 .constrained()
-//                 .with_width(avatar_style.outer_width)
-//                 .into_any()
-//         })
-//         .contained()
-//         .with_style(container)
-//         .into_any()
-// }
-
-fn is_channels_feature_enabled(cx: &gpui::WindowContext<'_>) -> bool {
-    cx.is_staff() || cx.has_flag::<ChannelsAlpha>()
-}

crates/collab_ui2/src/face_pile.rs 🔗

@@ -1,30 +0,0 @@
-use gpui::{
-    div, AnyElement, ElementId, IntoElement, ParentElement, RenderOnce, Styled, WindowContext,
-};
-use smallvec::SmallVec;
-
-#[derive(Default, IntoElement)]
-pub struct FacePile {
-    pub faces: SmallVec<[AnyElement; 2]>,
-}
-
-impl RenderOnce for FacePile {
-    fn render(self, _: &mut WindowContext) -> impl IntoElement {
-        let player_count = self.faces.len();
-        let player_list = self.faces.into_iter().enumerate().map(|(ix, player)| {
-            let isnt_last = ix < player_count - 1;
-
-            div()
-                .z_index((player_count - ix) as u8)
-                .when(isnt_last, |div| div.neg_mr_1())
-                .child(player)
-        });
-        div().p_1().flex().items_center().children(player_list)
-    }
-}
-
-impl ParentElement for FacePile {
-    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
-        &mut self.faces
-    }
-}

crates/collab_ui2/src/notification_panel.rs 🔗

@@ -1,755 +0,0 @@
-use crate::{chat_panel::ChatPanel, NotificationPanelSettings};
-use anyhow::Result;
-use channel::ChannelStore;
-use client::{Client, Notification, User, UserStore};
-use collections::HashMap;
-use db::kvp::KEY_VALUE_STORE;
-use futures::StreamExt;
-use gpui::{
-    actions, div, img, list, px, serde_json, AnyElement, AppContext, AsyncWindowContext,
-    CursorStyle, DismissEvent, Element, EventEmitter, FocusHandle, FocusableView,
-    InteractiveElement, IntoElement, ListAlignment, ListScrollEvent, ListState, Model,
-    ParentElement, Render, StatefulInteractiveElement, Styled, Task, View, ViewContext,
-    VisualContext, WeakView, WindowContext,
-};
-use notifications::{NotificationEntry, NotificationEvent, NotificationStore};
-use project::Fs;
-use rpc::proto;
-use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsStore};
-use std::{sync::Arc, time::Duration};
-use time::{OffsetDateTime, UtcOffset};
-use ui::{h_stack, prelude::*, v_stack, Avatar, Button, Icon, IconButton, IconElement, Label};
-use util::{ResultExt, TryFutureExt};
-use workspace::{
-    dock::{DockPosition, Panel, PanelEvent},
-    Workspace,
-};
-
-const LOADING_THRESHOLD: usize = 30;
-const MARK_AS_READ_DELAY: Duration = Duration::from_secs(1);
-const TOAST_DURATION: Duration = Duration::from_secs(5);
-const NOTIFICATION_PANEL_KEY: &'static str = "NotificationPanel";
-
-pub struct NotificationPanel {
-    client: Arc<Client>,
-    user_store: Model<UserStore>,
-    channel_store: Model<ChannelStore>,
-    notification_store: Model<NotificationStore>,
-    fs: Arc<dyn Fs>,
-    width: Option<Pixels>,
-    active: bool,
-    notification_list: ListState,
-    pending_serialization: Task<Option<()>>,
-    subscriptions: Vec<gpui::Subscription>,
-    workspace: WeakView<Workspace>,
-    current_notification_toast: Option<(u64, Task<()>)>,
-    local_timezone: UtcOffset,
-    focus_handle: FocusHandle,
-    mark_as_read_tasks: HashMap<u64, Task<Result<()>>>,
-}
-
-#[derive(Serialize, Deserialize)]
-struct SerializedNotificationPanel {
-    width: Option<Pixels>,
-}
-
-#[derive(Debug)]
-pub enum Event {
-    DockPositionChanged,
-    Focus,
-    Dismissed,
-}
-
-pub struct NotificationPresenter {
-    pub actor: Option<Arc<client::User>>,
-    pub text: String,
-    pub icon: &'static str,
-    pub needs_response: bool,
-    pub can_navigate: bool,
-}
-
-actions!(notification_panel, [ToggleFocus]);
-
-pub fn init(cx: &mut AppContext) {
-    cx.observe_new_views(|workspace: &mut Workspace, _| {
-        workspace.register_action(|workspace, _: &ToggleFocus, cx| {
-            workspace.toggle_panel_focus::<NotificationPanel>(cx);
-        });
-    })
-    .detach();
-}
-
-impl NotificationPanel {
-    pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
-        let fs = workspace.app_state().fs.clone();
-        let client = workspace.app_state().client.clone();
-        let user_store = workspace.app_state().user_store.clone();
-        let workspace_handle = workspace.weak_handle();
-
-        cx.new_view(|cx: &mut ViewContext<Self>| {
-            let mut status = client.status();
-            cx.spawn(|this, mut cx| async move {
-                while let Some(_) = status.next().await {
-                    if this
-                        .update(&mut cx, |_, cx| {
-                            cx.notify();
-                        })
-                        .is_err()
-                    {
-                        break;
-                    }
-                }
-            })
-            .detach();
-
-            let view = cx.view().downgrade();
-            let notification_list =
-                ListState::new(0, ListAlignment::Top, px(1000.), move |ix, cx| {
-                    view.upgrade()
-                        .and_then(|view| {
-                            view.update(cx, |this, cx| this.render_notification(ix, cx))
-                        })
-                        .unwrap_or_else(|| div().into_any())
-                });
-            notification_list.set_scroll_handler(cx.listener(
-                |this, event: &ListScrollEvent, cx| {
-                    if event.count.saturating_sub(event.visible_range.end) < LOADING_THRESHOLD {
-                        if let Some(task) = this
-                            .notification_store
-                            .update(cx, |store, cx| store.load_more_notifications(false, cx))
-                        {
-                            task.detach();
-                        }
-                    }
-                },
-            ));
-
-            let mut this = Self {
-                fs,
-                client,
-                user_store,
-                local_timezone: cx.local_timezone(),
-                channel_store: ChannelStore::global(cx),
-                notification_store: NotificationStore::global(cx),
-                notification_list,
-                pending_serialization: Task::ready(None),
-                workspace: workspace_handle,
-                focus_handle: cx.focus_handle(),
-                current_notification_toast: None,
-                subscriptions: Vec::new(),
-                active: false,
-                mark_as_read_tasks: HashMap::default(),
-                width: None,
-            };
-
-            let mut old_dock_position = this.position(cx);
-            this.subscriptions.extend([
-                cx.observe(&this.notification_store, |_, _, cx| cx.notify()),
-                cx.subscribe(&this.notification_store, Self::on_notification_event),
-                cx.observe_global::<SettingsStore>(move |this: &mut Self, cx| {
-                    let new_dock_position = this.position(cx);
-                    if new_dock_position != old_dock_position {
-                        old_dock_position = new_dock_position;
-                        cx.emit(Event::DockPositionChanged);
-                    }
-                    cx.notify();
-                }),
-            ]);
-            this
-        })
-    }
-
-    pub fn load(
-        workspace: WeakView<Workspace>,
-        cx: AsyncWindowContext,
-    ) -> Task<Result<View<Self>>> {
-        cx.spawn(|mut cx| async move {
-            let serialized_panel = if let Some(panel) = cx
-                .background_executor()
-                .spawn(async move { KEY_VALUE_STORE.read_kvp(NOTIFICATION_PANEL_KEY) })
-                .await
-                .log_err()
-                .flatten()
-            {
-                Some(serde_json::from_str::<SerializedNotificationPanel>(&panel)?)
-            } else {
-                None
-            };
-
-            workspace.update(&mut cx, |workspace, cx| {
-                let panel = Self::new(workspace, cx);
-                if let Some(serialized_panel) = serialized_panel {
-                    panel.update(cx, |panel, cx| {
-                        panel.width = serialized_panel.width;
-                        cx.notify();
-                    });
-                }
-                panel
-            })
-        })
-    }
-
-    fn serialize(&mut self, cx: &mut ViewContext<Self>) {
-        let width = self.width;
-        self.pending_serialization = cx.background_executor().spawn(
-            async move {
-                KEY_VALUE_STORE
-                    .write_kvp(
-                        NOTIFICATION_PANEL_KEY.into(),
-                        serde_json::to_string(&SerializedNotificationPanel { width })?,
-                    )
-                    .await?;
-                anyhow::Ok(())
-            }
-            .log_err(),
-        );
-    }
-
-    fn render_notification(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
-        let entry = self.notification_store.read(cx).notification_at(ix)?;
-        let notification_id = entry.id;
-        let now = OffsetDateTime::now_utc();
-        let timestamp = entry.timestamp;
-        let NotificationPresenter {
-            actor,
-            text,
-            needs_response,
-            can_navigate,
-            ..
-        } = self.present_notification(entry, cx)?;
-
-        let response = entry.response;
-        let notification = entry.notification.clone();
-
-        if self.active && !entry.is_read {
-            self.did_render_notification(notification_id, &notification, cx);
-        }
-
-        Some(
-            div()
-                .id(ix)
-                .flex()
-                .flex_row()
-                .size_full()
-                .px_2()
-                .py_1()
-                .gap_2()
-                .when(can_navigate, |el| {
-                    el.cursor(CursorStyle::PointingHand).on_click({
-                        let notification = notification.clone();
-                        cx.listener(move |this, _, cx| {
-                            this.did_click_notification(&notification, cx)
-                        })
-                    })
-                })
-                .children(actor.map(|actor| {
-                    img(actor.avatar_uri.clone())
-                        .flex_none()
-                        .w_8()
-                        .h_8()
-                        .rounded_full()
-                }))
-                .child(
-                    v_stack()
-                        .gap_1()
-                        .size_full()
-                        .overflow_hidden()
-                        .child(Label::new(text.clone()))
-                        .child(
-                            h_stack()
-                                .child(
-                                    Label::new(format_timestamp(
-                                        timestamp,
-                                        now,
-                                        self.local_timezone,
-                                    ))
-                                    .color(Color::Muted),
-                                )
-                                .children(if let Some(is_accepted) = response {
-                                    Some(div().flex().flex_grow().justify_end().child(Label::new(
-                                        if is_accepted {
-                                            "You accepted"
-                                        } else {
-                                            "You declined"
-                                        },
-                                    )))
-                                } else if needs_response {
-                                    Some(
-                                        h_stack()
-                                            .flex_grow()
-                                            .justify_end()
-                                            .child(Button::new("decline", "Decline").on_click({
-                                                let notification = notification.clone();
-                                                let view = cx.view().clone();
-                                                move |_, cx| {
-                                                    view.update(cx, |this, cx| {
-                                                        this.respond_to_notification(
-                                                            notification.clone(),
-                                                            false,
-                                                            cx,
-                                                        )
-                                                    });
-                                                }
-                                            }))
-                                            .child(Button::new("accept", "Accept").on_click({
-                                                let notification = notification.clone();
-                                                let view = cx.view().clone();
-                                                move |_, cx| {
-                                                    view.update(cx, |this, cx| {
-                                                        this.respond_to_notification(
-                                                            notification.clone(),
-                                                            true,
-                                                            cx,
-                                                        )
-                                                    });
-                                                }
-                                            })),
-                                    )
-                                } else {
-                                    None
-                                }),
-                        ),
-                )
-                .into_any(),
-        )
-    }
-
-    fn present_notification(
-        &self,
-        entry: &NotificationEntry,
-        cx: &AppContext,
-    ) -> Option<NotificationPresenter> {
-        let user_store = self.user_store.read(cx);
-        let channel_store = self.channel_store.read(cx);
-        match entry.notification {
-            Notification::ContactRequest { sender_id } => {
-                let requester = user_store.get_cached_user(sender_id)?;
-                Some(NotificationPresenter {
-                    icon: "icons/plus.svg",
-                    text: format!("{} wants to add you as a contact", requester.github_login),
-                    needs_response: user_store.has_incoming_contact_request(requester.id),
-                    actor: Some(requester),
-                    can_navigate: false,
-                })
-            }
-            Notification::ContactRequestAccepted { responder_id } => {
-                let responder = user_store.get_cached_user(responder_id)?;
-                Some(NotificationPresenter {
-                    icon: "icons/plus.svg",
-                    text: format!("{} accepted your contact invite", responder.github_login),
-                    needs_response: false,
-                    actor: Some(responder),
-                    can_navigate: false,
-                })
-            }
-            Notification::ChannelInvitation {
-                ref channel_name,
-                channel_id,
-                inviter_id,
-            } => {
-                let inviter = user_store.get_cached_user(inviter_id)?;
-                Some(NotificationPresenter {
-                    icon: "icons/hash.svg",
-                    text: format!(
-                        "{} invited you to join the #{channel_name} channel",
-                        inviter.github_login
-                    ),
-                    needs_response: channel_store.has_channel_invitation(channel_id),
-                    actor: Some(inviter),
-                    can_navigate: false,
-                })
-            }
-            Notification::ChannelMessageMention {
-                sender_id,
-                channel_id,
-                message_id,
-            } => {
-                let sender = user_store.get_cached_user(sender_id)?;
-                let channel = channel_store.channel_for_id(channel_id)?;
-                let message = self
-                    .notification_store
-                    .read(cx)
-                    .channel_message_for_id(message_id)?;
-                Some(NotificationPresenter {
-                    icon: "icons/conversations.svg",
-                    text: format!(
-                        "{} mentioned you in #{}:\n{}",
-                        sender.github_login, channel.name, message.body,
-                    ),
-                    needs_response: false,
-                    actor: Some(sender),
-                    can_navigate: true,
-                })
-            }
-        }
-    }
-
-    fn did_render_notification(
-        &mut self,
-        notification_id: u64,
-        notification: &Notification,
-        cx: &mut ViewContext<Self>,
-    ) {
-        let should_mark_as_read = match notification {
-            Notification::ContactRequestAccepted { .. } => true,
-            Notification::ContactRequest { .. }
-            | Notification::ChannelInvitation { .. }
-            | Notification::ChannelMessageMention { .. } => false,
-        };
-
-        if should_mark_as_read {
-            self.mark_as_read_tasks
-                .entry(notification_id)
-                .or_insert_with(|| {
-                    let client = self.client.clone();
-                    cx.spawn(|this, mut cx| async move {
-                        cx.background_executor().timer(MARK_AS_READ_DELAY).await;
-                        client
-                            .request(proto::MarkNotificationRead { notification_id })
-                            .await?;
-                        this.update(&mut cx, |this, _| {
-                            this.mark_as_read_tasks.remove(&notification_id);
-                        })?;
-                        Ok(())
-                    })
-                });
-        }
-    }
-
-    fn did_click_notification(&mut self, notification: &Notification, cx: &mut ViewContext<Self>) {
-        if let Notification::ChannelMessageMention {
-            message_id,
-            channel_id,
-            ..
-        } = notification.clone()
-        {
-            if let Some(workspace) = self.workspace.upgrade() {
-                cx.window_context().defer(move |cx| {
-                    workspace.update(cx, |workspace, cx| {
-                        if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
-                            panel.update(cx, |panel, cx| {
-                                panel
-                                    .select_channel(channel_id, Some(message_id), cx)
-                                    .detach_and_log_err(cx);
-                            });
-                        }
-                    });
-                });
-            }
-        }
-    }
-
-    fn is_showing_notification(&self, notification: &Notification, cx: &ViewContext<Self>) -> bool {
-        if let Notification::ChannelMessageMention { channel_id, .. } = &notification {
-            if let Some(workspace) = self.workspace.upgrade() {
-                return if let Some(panel) = workspace.read(cx).panel::<ChatPanel>(cx) {
-                    let panel = panel.read(cx);
-                    panel.is_scrolled_to_bottom()
-                        && panel
-                            .active_chat()
-                            .map_or(false, |chat| chat.read(cx).channel_id == *channel_id)
-                } else {
-                    false
-                };
-            }
-        }
-
-        false
-    }
-
-    fn on_notification_event(
-        &mut self,
-        _: Model<NotificationStore>,
-        event: &NotificationEvent,
-        cx: &mut ViewContext<Self>,
-    ) {
-        match event {
-            NotificationEvent::NewNotification { entry } => self.add_toast(entry, cx),
-            NotificationEvent::NotificationRemoved { entry }
-            | NotificationEvent::NotificationRead { entry } => self.remove_toast(entry.id, cx),
-            NotificationEvent::NotificationsUpdated {
-                old_range,
-                new_count,
-            } => {
-                self.notification_list.splice(old_range.clone(), *new_count);
-                cx.notify();
-            }
-        }
-    }
-
-    fn add_toast(&mut self, entry: &NotificationEntry, cx: &mut ViewContext<Self>) {
-        if self.is_showing_notification(&entry.notification, cx) {
-            return;
-        }
-
-        let Some(NotificationPresenter { actor, text, .. }) = self.present_notification(entry, cx)
-        else {
-            return;
-        };
-
-        let notification_id = entry.id;
-        self.current_notification_toast = Some((
-            notification_id,
-            cx.spawn(|this, mut cx| async move {
-                cx.background_executor().timer(TOAST_DURATION).await;
-                this.update(&mut cx, |this, cx| this.remove_toast(notification_id, cx))
-                    .ok();
-            }),
-        ));
-
-        self.workspace
-            .update(cx, |workspace, cx| {
-                workspace.dismiss_notification::<NotificationToast>(0, cx);
-                workspace.show_notification(0, cx, |cx| {
-                    let workspace = cx.view().downgrade();
-                    cx.new_view(|_| NotificationToast {
-                        notification_id,
-                        actor,
-                        text,
-                        workspace,
-                    })
-                })
-            })
-            .ok();
-    }
-
-    fn remove_toast(&mut self, notification_id: u64, cx: &mut ViewContext<Self>) {
-        if let Some((current_id, _)) = &self.current_notification_toast {
-            if *current_id == notification_id {
-                self.current_notification_toast.take();
-                self.workspace
-                    .update(cx, |workspace, cx| {
-                        workspace.dismiss_notification::<NotificationToast>(0, cx)
-                    })
-                    .ok();
-            }
-        }
-    }
-
-    fn respond_to_notification(
-        &mut self,
-        notification: Notification,
-        response: bool,
-        cx: &mut ViewContext<Self>,
-    ) {
-        self.notification_store.update(cx, |store, cx| {
-            store.respond_to_notification(notification, response, cx);
-        });
-    }
-}
-
-impl Render for NotificationPanel {
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
-        v_stack()
-            .size_full()
-            .child(
-                h_stack()
-                    .justify_between()
-                    .px_2()
-                    .py_1()
-                    // Match the height of the tab bar so they line up.
-                    .h(rems(ui::Tab::HEIGHT_IN_REMS))
-                    .border_b_1()
-                    .border_color(cx.theme().colors().border)
-                    .child(Label::new("Notifications"))
-                    .child(IconElement::new(Icon::Envelope)),
-            )
-            .map(|this| {
-                if self.client.user_id().is_none() {
-                    this.child(
-                        v_stack()
-                            .gap_2()
-                            .p_4()
-                            .child(
-                                Button::new("sign_in_prompt_button", "Sign in")
-                                    .icon_color(Color::Muted)
-                                    .icon(Icon::Github)
-                                    .icon_position(IconPosition::Start)
-                                    .style(ButtonStyle::Filled)
-                                    .full_width()
-                                    .on_click({
-                                        let client = self.client.clone();
-                                        move |_, cx| {
-                                            let client = client.clone();
-                                            cx.spawn(move |cx| async move {
-                                                client
-                                                    .authenticate_and_connect(true, &cx)
-                                                    .log_err()
-                                                    .await;
-                                            })
-                                            .detach()
-                                        }
-                                    }),
-                            )
-                            .child(
-                                div().flex().w_full().items_center().child(
-                                    Label::new("Sign in to view notifications.")
-                                        .color(Color::Muted)
-                                        .size(LabelSize::Small),
-                                ),
-                            ),
-                    )
-                } else if self.notification_list.item_count() == 0 {
-                    this.child(
-                        v_stack().p_4().child(
-                            div().flex().w_full().items_center().child(
-                                Label::new("You have no notifications.")
-                                    .color(Color::Muted)
-                                    .size(LabelSize::Small),
-                            ),
-                        ),
-                    )
-                } else {
-                    this.child(list(self.notification_list.clone()).size_full())
-                }
-            })
-    }
-}
-
-impl FocusableView for NotificationPanel {
-    fn focus_handle(&self, _: &AppContext) -> FocusHandle {
-        self.focus_handle.clone()
-    }
-}
-
-impl EventEmitter<Event> for NotificationPanel {}
-impl EventEmitter<PanelEvent> for NotificationPanel {}
-
-impl Panel for NotificationPanel {
-    fn persistent_name() -> &'static str {
-        "NotificationPanel"
-    }
-
-    fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
-        NotificationPanelSettings::get_global(cx).dock
-    }
-
-    fn position_is_valid(&self, position: DockPosition) -> bool {
-        matches!(position, DockPosition::Left | DockPosition::Right)
-    }
-
-    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
-        settings::update_settings_file::<NotificationPanelSettings>(
-            self.fs.clone(),
-            cx,
-            move |settings| settings.dock = Some(position),
-        );
-    }
-
-    fn size(&self, cx: &gpui::WindowContext) -> Pixels {
-        self.width
-            .unwrap_or_else(|| NotificationPanelSettings::get_global(cx).default_width)
-    }
-
-    fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
-        self.width = size;
-        self.serialize(cx);
-        cx.notify();
-    }
-
-    fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
-        self.active = active;
-        if self.notification_store.read(cx).notification_count() == 0 {
-            cx.emit(Event::Dismissed);
-        }
-    }
-
-    fn icon(&self, cx: &gpui::WindowContext) -> Option<Icon> {
-        (NotificationPanelSettings::get_global(cx).button
-            && self.notification_store.read(cx).notification_count() > 0)
-            .then(|| Icon::Bell)
-    }
-
-    fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
-        Some("Notification Panel")
-    }
-
-    fn icon_label(&self, cx: &WindowContext) -> Option<String> {
-        let count = self.notification_store.read(cx).unread_notification_count();
-        if count == 0 {
-            None
-        } else {
-            Some(count.to_string())
-        }
-    }
-
-    fn toggle_action(&self) -> Box<dyn gpui::Action> {
-        Box::new(ToggleFocus)
-    }
-}
-
-pub struct NotificationToast {
-    notification_id: u64,
-    actor: Option<Arc<User>>,
-    text: String,
-    workspace: WeakView<Workspace>,
-}
-
-impl NotificationToast {
-    fn focus_notification_panel(&self, cx: &mut ViewContext<Self>) {
-        let workspace = self.workspace.clone();
-        let notification_id = self.notification_id;
-        cx.window_context().defer(move |cx| {
-            workspace
-                .update(cx, |workspace, cx| {
-                    if let Some(panel) = workspace.focus_panel::<NotificationPanel>(cx) {
-                        panel.update(cx, |panel, cx| {
-                            let store = panel.notification_store.read(cx);
-                            if let Some(entry) = store.notification_for_id(notification_id) {
-                                panel.did_click_notification(&entry.clone().notification, cx);
-                            }
-                        });
-                    }
-                })
-                .ok();
-        })
-    }
-}
-
-impl Render for NotificationToast {
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
-        let user = self.actor.clone();
-
-        h_stack()
-            .id("notification_panel_toast")
-            .children(user.map(|user| Avatar::new(user.avatar_uri.clone())))
-            .child(Label::new(self.text.clone()))
-            .child(
-                IconButton::new("close", Icon::Close)
-                    .on_click(cx.listener(|_, _, cx| cx.emit(DismissEvent))),
-            )
-            .on_click(cx.listener(|this, _, cx| {
-                this.focus_notification_panel(cx);
-                cx.emit(DismissEvent);
-            }))
-    }
-}
-
-impl EventEmitter<DismissEvent> for NotificationToast {}
-
-fn format_timestamp(
-    mut timestamp: OffsetDateTime,
-    mut now: OffsetDateTime,
-    local_timezone: UtcOffset,
-) -> String {
-    timestamp = timestamp.to_offset(local_timezone);
-    now = now.to_offset(local_timezone);
-
-    let today = now.date();
-    let date = timestamp.date();
-    if date == today {
-        let difference = now - timestamp;
-        if difference >= Duration::from_secs(3600) {
-            format!("{}h", difference.whole_seconds() / 3600)
-        } else if difference >= Duration::from_secs(60) {
-            format!("{}m", difference.whole_seconds() / 60)
-        } else {
-            "just now".to_string()
-        }
-    } else if date.next_day() == Some(today) {
-        format!("yesterday")
-    } else {
-        format!("{:02}/{}/{}", date.month() as u32, date.day(), date.year())
-    }
-}

crates/collab_ui2/src/notifications.rs 🔗

@@ -1,11 +0,0 @@
-use gpui::AppContext;
-use std::sync::Arc;
-use workspace::AppState;
-
-pub mod incoming_call_notification;
-pub mod project_shared_notification;
-
-pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
-    incoming_call_notification::init(app_state, cx);
-    project_shared_notification::init(app_state, cx);
-}

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

@@ -1,163 +0,0 @@
-use crate::notification_window_options;
-use call::{ActiveCall, IncomingCall};
-use futures::StreamExt;
-use gpui::{
-    img, px, AppContext, ParentElement, Render, RenderOnce, Styled, ViewContext,
-    VisualContext as _, WindowHandle,
-};
-use settings::Settings;
-use std::sync::{Arc, Weak};
-use theme::ThemeSettings;
-use ui::prelude::*;
-use ui::{h_stack, v_stack, Button, Label};
-use util::ResultExt;
-use workspace::AppState;
-
-pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
-    let app_state = Arc::downgrade(app_state);
-    let mut incoming_call = ActiveCall::global(cx).read(cx).incoming();
-    cx.spawn(|mut cx| async move {
-        let mut notification_windows: Vec<WindowHandle<IncomingCallNotification>> = Vec::new();
-        while let Some(incoming_call) = incoming_call.next().await {
-            for window in notification_windows.drain(..) {
-                window
-                    .update(&mut cx, |_, cx| {
-                        // todo!()
-                        cx.remove_window();
-                    })
-                    .log_err();
-            }
-
-            if let Some(incoming_call) = incoming_call {
-                let unique_screens = cx.update(|cx| cx.displays()).unwrap();
-                let window_size = gpui::Size {
-                    width: px(380.),
-                    height: px(64.),
-                };
-
-                for screen in unique_screens {
-                    let options = notification_window_options(screen, window_size);
-                    let window = cx
-                        .open_window(options, |cx| {
-                            cx.new_view(|_| {
-                                IncomingCallNotification::new(
-                                    incoming_call.clone(),
-                                    app_state.clone(),
-                                )
-                            })
-                        })
-                        .unwrap();
-                    notification_windows.push(window);
-                }
-            }
-        }
-    })
-    .detach();
-}
-
-#[derive(Clone, PartialEq)]
-struct RespondToCall {
-    accept: bool,
-}
-
-struct IncomingCallNotificationState {
-    call: IncomingCall,
-    app_state: Weak<AppState>,
-}
-
-pub struct IncomingCallNotification {
-    state: Arc<IncomingCallNotificationState>,
-}
-impl IncomingCallNotificationState {
-    pub fn new(call: IncomingCall, app_state: Weak<AppState>) -> Self {
-        Self { call, app_state }
-    }
-
-    fn respond(&self, accept: bool, cx: &mut AppContext) {
-        let active_call = ActiveCall::global(cx);
-        if accept {
-            let join = active_call.update(cx, |active_call, cx| active_call.accept_incoming(cx));
-            let caller_user_id = self.call.calling_user.id;
-            let initial_project_id = self.call.initial_project.as_ref().map(|project| project.id);
-            let app_state = self.app_state.clone();
-            let cx: &mut AppContext = cx;
-            cx.spawn(|cx| async move {
-                join.await?;
-                if let Some(project_id) = initial_project_id {
-                    cx.update(|cx| {
-                        if let Some(app_state) = app_state.upgrade() {
-                            workspace::join_remote_project(
-                                project_id,
-                                caller_user_id,
-                                app_state,
-                                cx,
-                            )
-                            .detach_and_log_err(cx);
-                        }
-                    })
-                    .log_err();
-                }
-                anyhow::Ok(())
-            })
-            .detach_and_log_err(cx);
-        } else {
-            active_call.update(cx, |active_call, cx| {
-                active_call.decline_incoming(cx).log_err();
-            });
-        }
-    }
-}
-
-impl IncomingCallNotification {
-    pub fn new(call: IncomingCall, app_state: Weak<AppState>) -> Self {
-        Self {
-            state: Arc::new(IncomingCallNotificationState::new(call, app_state)),
-        }
-    }
-}
-
-impl Render for IncomingCallNotification {
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
-        // TODO: Is there a better place for us to initialize the font?
-        let (ui_font, ui_font_size) = {
-            let theme_settings = ThemeSettings::get_global(cx);
-            (
-                theme_settings.ui_font.family.clone(),
-                theme_settings.ui_font_size.clone(),
-            )
-        };
-
-        cx.set_rem_size(ui_font_size);
-
-        h_stack()
-            .font(ui_font)
-            .text_ui()
-            .justify_between()
-            .size_full()
-            .overflow_hidden()
-            .elevation_3(cx)
-            .p_2()
-            .gap_2()
-            .child(
-                img(self.state.call.calling_user.avatar_uri.clone())
-                    .w_12()
-                    .h_12()
-                    .rounded_full(),
-            )
-            .child(v_stack().overflow_hidden().child(Label::new(format!(
-                "{} is sharing a project in Zed",
-                self.state.call.calling_user.github_login
-            ))))
-            .child(
-                v_stack()
-                    .child(Button::new("accept", "Accept").render(cx).on_click({
-                        let state = self.state.clone();
-                        move |_, cx| state.respond(true, cx)
-                    }))
-                    .child(Button::new("decline", "Decline").render(cx).on_click({
-                        let state = self.state.clone();
-                        move |_, cx| state.respond(false, cx)
-                    })),
-            )
-    }
-}

crates/collab_ui2/src/notifications/project_shared_notification.rs 🔗

@@ -1,180 +0,0 @@
-use crate::notification_window_options;
-use call::{room, ActiveCall};
-use client::User;
-use collections::HashMap;
-use gpui::{img, px, AppContext, ParentElement, Render, Size, Styled, ViewContext, VisualContext};
-use settings::Settings;
-use std::sync::{Arc, Weak};
-use theme::ThemeSettings;
-use ui::{h_stack, prelude::*, v_stack, Button, Label};
-use workspace::AppState;
-
-pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
-    let app_state = Arc::downgrade(app_state);
-    let active_call = ActiveCall::global(cx);
-    let mut notification_windows = HashMap::default();
-    cx.subscribe(&active_call, move |_, event, cx| match event {
-        room::Event::RemoteProjectShared {
-            owner,
-            project_id,
-            worktree_root_names,
-        } => {
-            let window_size = Size {
-                width: px(400.),
-                height: px(72.),
-            };
-
-            for screen in cx.displays() {
-                let options = notification_window_options(screen, window_size);
-                let window = cx.open_window(options, |cx| {
-                    cx.new_view(|_| {
-                        ProjectSharedNotification::new(
-                            owner.clone(),
-                            *project_id,
-                            worktree_root_names.clone(),
-                            app_state.clone(),
-                        )
-                    })
-                });
-                notification_windows
-                    .entry(*project_id)
-                    .or_insert(Vec::new())
-                    .push(window);
-            }
-        }
-
-        room::Event::RemoteProjectUnshared { project_id }
-        | room::Event::RemoteProjectJoined { project_id }
-        | room::Event::RemoteProjectInvitationDiscarded { project_id } => {
-            if let Some(windows) = notification_windows.remove(&project_id) {
-                for window in windows {
-                    window
-                        .update(cx, |_, cx| {
-                            // todo!()
-                            cx.remove_window();
-                        })
-                        .ok();
-                }
-            }
-        }
-
-        room::Event::Left => {
-            for (_, windows) in notification_windows.drain() {
-                for window in windows {
-                    window
-                        .update(cx, |_, cx| {
-                            // todo!()
-                            cx.remove_window();
-                        })
-                        .ok();
-                }
-            }
-        }
-        _ => {}
-    })
-    .detach();
-}
-
-pub struct ProjectSharedNotification {
-    project_id: u64,
-    worktree_root_names: Vec<String>,
-    owner: Arc<User>,
-    app_state: Weak<AppState>,
-}
-
-impl ProjectSharedNotification {
-    fn new(
-        owner: Arc<User>,
-        project_id: u64,
-        worktree_root_names: Vec<String>,
-        app_state: Weak<AppState>,
-    ) -> Self {
-        Self {
-            project_id,
-            worktree_root_names,
-            owner,
-            app_state,
-        }
-    }
-
-    fn join(&mut self, cx: &mut ViewContext<Self>) {
-        if let Some(app_state) = self.app_state.upgrade() {
-            workspace::join_remote_project(self.project_id, self.owner.id, app_state, cx)
-                .detach_and_log_err(cx);
-        }
-    }
-
-    fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
-        if let Some(active_room) =
-            ActiveCall::global(cx).read_with(cx, |call, _| call.room().cloned())
-        {
-            active_room.update(cx, |_, cx| {
-                cx.emit(room::Event::RemoteProjectInvitationDiscarded {
-                    project_id: self.project_id,
-                });
-            });
-        }
-    }
-}
-
-impl Render for ProjectSharedNotification {
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
-        // TODO: Is there a better place for us to initialize the font?
-        let (ui_font, ui_font_size) = {
-            let theme_settings = ThemeSettings::get_global(cx);
-            (
-                theme_settings.ui_font.family.clone(),
-                theme_settings.ui_font_size.clone(),
-            )
-        };
-
-        cx.set_rem_size(ui_font_size);
-
-        h_stack()
-            .font(ui_font)
-            .text_ui()
-            .justify_between()
-            .size_full()
-            .overflow_hidden()
-            .elevation_3(cx)
-            .p_2()
-            .gap_2()
-            .child(
-                img(self.owner.avatar_uri.clone())
-                    .w_12()
-                    .h_12()
-                    .rounded_full(),
-            )
-            .child(
-                v_stack()
-                    .overflow_hidden()
-                    .child(Label::new(self.owner.github_login.clone()))
-                    .child(Label::new(format!(
-                        "is sharing a project in Zed{}",
-                        if self.worktree_root_names.is_empty() {
-                            ""
-                        } else {
-                            ":"
-                        }
-                    )))
-                    .children(if self.worktree_root_names.is_empty() {
-                        None
-                    } else {
-                        Some(Label::new(self.worktree_root_names.join(", ")))
-                    }),
-            )
-            .child(
-                v_stack()
-                    .child(Button::new("open", "Open").on_click(cx.listener(
-                        move |this, _event, cx| {
-                            this.join(cx);
-                        },
-                    )))
-                    .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
-                        move |this, _event, cx| {
-                            this.dismiss(cx);
-                        },
-                    ))),
-            )
-    }
-}

crates/collab_ui2/src/panel_settings.rs 🔗

@@ -1,70 +0,0 @@
-use anyhow;
-use gpui::Pixels;
-use schemars::JsonSchema;
-use serde_derive::{Deserialize, Serialize};
-use settings::Settings;
-use workspace::dock::DockPosition;
-
-#[derive(Deserialize, Debug)]
-pub struct CollaborationPanelSettings {
-    pub button: bool,
-    pub dock: DockPosition,
-    pub default_width: Pixels,
-}
-
-#[derive(Deserialize, Debug)]
-pub struct ChatPanelSettings {
-    pub button: bool,
-    pub dock: DockPosition,
-    pub default_width: Pixels,
-}
-
-#[derive(Deserialize, Debug)]
-pub struct NotificationPanelSettings {
-    pub button: bool,
-    pub dock: DockPosition,
-    pub default_width: Pixels,
-}
-
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
-pub struct PanelSettingsContent {
-    pub button: Option<bool>,
-    pub dock: Option<DockPosition>,
-    pub default_width: Option<f32>,
-}
-
-impl Settings for CollaborationPanelSettings {
-    const KEY: Option<&'static str> = Some("collaboration_panel");
-    type FileContent = PanelSettingsContent;
-    fn load(
-        default_value: &Self::FileContent,
-        user_values: &[&Self::FileContent],
-        _: &mut gpui::AppContext,
-    ) -> anyhow::Result<Self> {
-        Self::load_via_json_merge(default_value, user_values)
-    }
-}
-
-impl Settings for ChatPanelSettings {
-    const KEY: Option<&'static str> = Some("chat_panel");
-    type FileContent = PanelSettingsContent;
-    fn load(
-        default_value: &Self::FileContent,
-        user_values: &[&Self::FileContent],
-        _: &mut gpui::AppContext,
-    ) -> anyhow::Result<Self> {
-        Self::load_via_json_merge(default_value, user_values)
-    }
-}
-
-impl Settings for NotificationPanelSettings {
-    const KEY: Option<&'static str> = Some("notification_panel");
-    type FileContent = PanelSettingsContent;
-    fn load(
-        default_value: &Self::FileContent,
-        user_values: &[&Self::FileContent],
-        _: &mut gpui::AppContext,
-    ) -> anyhow::Result<Self> {
-        Self::load_via_json_merge(default_value, user_values)
-    }
-}

crates/quick_action_bar/Cargo.toml 🔗

@@ -9,15 +9,14 @@ path = "src/quick_action_bar.rs"
 doctest = false
 
 [dependencies]
-assistant = { path = "../assistant" }
-editor = { path = "../editor" }
-gpui = { path = "../gpui" }
-search = { path = "../search" }
-theme = { path = "../theme" }
-workspace = { path = "../workspace" }
+assistant = { package = "assistant2", path = "../assistant2" }
+editor = { package = "editor2", path = "../editor2" }
+gpui = { package = "gpui2", path = "../gpui2" }
+search = { package = "search2", path = "../search2" }
+workspace = { package = "workspace2", path = "../workspace2" }
+ui = { package = "ui2", path = "../ui2" }
 
 [dev-dependencies]
-editor = { path = "../editor", features = ["test-support"] }
-gpui = { path = "../gpui", features = ["test-support"] }
-theme = { path = "../theme", features = ["test-support"] }
-workspace = { path = "../workspace", features = ["test-support"] }
+editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
+gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
+workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }

crates/quick_action_bar/src/quick_action_bar.rs 🔗

@@ -1,155 +1,153 @@
-use assistant::{assistant_panel::InlineAssist, AssistantPanel};
+use assistant::{AssistantPanel, InlineAssist};
 use editor::Editor;
+
 use gpui::{
-    elements::{Empty, Flex, MouseEventHandler, ParentElement, Svg},
-    platform::{CursorStyle, MouseButton},
-    Action, AnyElement, Element, Entity, EventContext, Subscription, View, ViewContext, ViewHandle,
-    WeakViewHandle,
+    Action, ClickEvent, ElementId, EventEmitter, InteractiveElement, ParentElement, Render, Styled,
+    Subscription, View, ViewContext, WeakView,
 };
-
 use search::{buffer_search, BufferSearchBar};
-use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView, Workspace};
+use ui::{prelude::*, ButtonSize, ButtonStyle, Icon, IconButton, IconSize, Tooltip};
+use workspace::{
+    item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
+};
 
 pub struct QuickActionBar {
-    buffer_search_bar: ViewHandle<BufferSearchBar>,
+    buffer_search_bar: View<BufferSearchBar>,
     active_item: Option<Box<dyn ItemHandle>>,
-    inlay_hints_enabled_subscription: Option<Subscription>,
-    workspace: WeakViewHandle<Workspace>,
+    _inlay_hints_enabled_subscription: Option<Subscription>,
+    workspace: WeakView<Workspace>,
 }
 
 impl QuickActionBar {
-    pub fn new(buffer_search_bar: ViewHandle<BufferSearchBar>, workspace: &Workspace) -> Self {
+    pub fn new(buffer_search_bar: View<BufferSearchBar>, workspace: &Workspace) -> Self {
         Self {
             buffer_search_bar,
             active_item: None,
-            inlay_hints_enabled_subscription: None,
+            _inlay_hints_enabled_subscription: None,
             workspace: workspace.weak_handle(),
         }
     }
 
-    fn active_editor(&self) -> Option<ViewHandle<Editor>> {
+    fn active_editor(&self) -> Option<View<Editor>> {
         self.active_item
             .as_ref()
             .and_then(|item| item.downcast::<Editor>())
     }
 }
 
-impl Entity for QuickActionBar {
-    type Event = ();
-}
-
-impl View for QuickActionBar {
-    fn ui_name() -> &'static str {
-        "QuickActionsBar"
-    }
-
-    fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
+impl Render for QuickActionBar {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
         let Some(editor) = self.active_editor() else {
-            return Empty::new().into_any();
+            return div().id("empty quick action bar");
         };
 
-        let mut bar = Flex::row();
-        if editor.read(cx).supports_inlay_hints(cx) {
-            bar = bar.with_child(render_quick_action_bar_button(
-                0,
-                "icons/inlay_hint.svg",
-                editor.read(cx).inlay_hints_enabled(),
-                (
-                    "Toggle Inlay Hints".to_string(),
-                    Some(Box::new(editor::ToggleInlayHints)),
-                ),
-                cx,
-                |this, cx| {
-                    if let Some(editor) = this.active_editor() {
-                        editor.update(cx, |editor, cx| {
-                            editor.toggle_inlay_hints(&editor::ToggleInlayHints, cx);
-                        });
-                    }
-                },
-            ));
-        }
-
-        if editor.read(cx).buffer().read(cx).is_singleton() {
-            let search_bar_shown = !self.buffer_search_bar.read(cx).is_dismissed();
-            let search_action = buffer_search::Deploy { focus: true };
-
-            bar = bar.with_child(render_quick_action_bar_button(
-                1,
-                "icons/magnifying_glass.svg",
-                search_bar_shown,
-                (
-                    "Buffer Search".to_string(),
-                    Some(Box::new(search_action.clone())),
-                ),
-                cx,
-                move |this, cx| {
-                    this.buffer_search_bar.update(cx, |buffer_search_bar, cx| {
-                        if search_bar_shown {
-                            buffer_search_bar.dismiss(&buffer_search::Dismiss, cx);
-                        } else {
-                            buffer_search_bar.deploy(&search_action, cx);
-                        }
+        let inlay_hints_button = Some(QuickActionBarButton::new(
+            "toggle inlay hints",
+            Icon::InlayHint,
+            editor.read(cx).inlay_hints_enabled(),
+            Box::new(editor::ToggleInlayHints),
+            "Toggle Inlay Hints",
+            {
+                let editor = editor.clone();
+                move |_, cx| {
+                    editor.update(cx, |editor, cx| {
+                        editor.toggle_inlay_hints(&editor::ToggleInlayHints, cx);
                     });
-                },
-            ));
-        }
-
-        bar.add_child(render_quick_action_bar_button(
-            2,
-            "icons/magic-wand.svg",
-            false,
-            ("Inline Assist".into(), Some(Box::new(InlineAssist))),
-            cx,
-            move |this, cx| {
-                if let Some(workspace) = this.workspace.upgrade(cx) {
-                    workspace.update(cx, |workspace, cx| {
-                        AssistantPanel::inline_assist(workspace, &Default::default(), cx);
+                }
+            },
+        ))
+        .filter(|_| editor.read(cx).supports_inlay_hints(cx));
+
+        let search_button = Some(QuickActionBarButton::new(
+            "toggle buffer search",
+            Icon::MagnifyingGlass,
+            !self.buffer_search_bar.read(cx).is_dismissed(),
+            Box::new(buffer_search::Deploy { focus: false }),
+            "Buffer Search",
+            {
+                let buffer_search_bar = self.buffer_search_bar.clone();
+                move |_, cx| {
+                    buffer_search_bar.update(cx, |search_bar, cx| {
+                        search_bar.toggle(&buffer_search::Deploy { focus: true }, cx)
                     });
                 }
             },
-        ));
+        ))
+        .filter(|_| editor.is_singleton(cx));
 
-        bar.into_any()
+        let assistant_button = QuickActionBarButton::new(
+            "toggle inline assistant",
+            Icon::MagicWand,
+            false,
+            Box::new(InlineAssist),
+            "Inline Assist",
+            {
+                let workspace = self.workspace.clone();
+                move |_, cx| {
+                    if let Some(workspace) = workspace.upgrade() {
+                        workspace.update(cx, |workspace, cx| {
+                            AssistantPanel::inline_assist(workspace, &InlineAssist, cx);
+                        });
+                    }
+                }
+            },
+        );
+
+        h_stack()
+            .id("quick action bar")
+            .p_1()
+            .gap_2()
+            .children(inlay_hints_button)
+            .children(search_button)
+            .child(assistant_button)
     }
 }
 
-fn render_quick_action_bar_button<
-    F: 'static + Fn(&mut QuickActionBar, &mut EventContext<QuickActionBar>),
->(
-    index: usize,
-    icon: &'static str,
+impl EventEmitter<ToolbarItemEvent> for QuickActionBar {}
+
+#[derive(IntoElement)]
+struct QuickActionBarButton {
+    id: ElementId,
+    icon: Icon,
     toggled: bool,
-    tooltip: (String, Option<Box<dyn Action>>),
-    cx: &mut ViewContext<QuickActionBar>,
-    on_click: F,
-) -> AnyElement<QuickActionBar> {
-    enum QuickActionBarButton {}
-
-    let theme = theme::current(cx);
-    let (tooltip_text, action) = tooltip;
-
-    MouseEventHandler::new::<QuickActionBarButton, _>(index, cx, |mouse_state, _| {
-        let style = theme
-            .workspace
-            .toolbar
-            .toggleable_tool
-            .in_state(toggled)
-            .style_for(mouse_state);
-        Svg::new(icon)
-            .with_color(style.color)
-            .constrained()
-            .with_width(style.icon_width)
-            .aligned()
-            .constrained()
-            .with_width(style.button_width)
-            .with_height(style.button_width)
-            .contained()
-            .with_style(style.container)
-    })
-    .with_cursor_style(CursorStyle::PointingHand)
-    .on_click(MouseButton::Left, move |_, pane, cx| on_click(pane, cx))
-    .with_tooltip::<QuickActionBarButton>(index, tooltip_text, action, theme.tooltip.clone(), cx)
-    .into_any_named("quick action bar button")
+    action: Box<dyn Action>,
+    tooltip: SharedString,
+    on_click: Box<dyn Fn(&ClickEvent, &mut WindowContext)>,
+}
+
+impl QuickActionBarButton {
+    fn new(
+        id: impl Into<ElementId>,
+        icon: Icon,
+        toggled: bool,
+        action: Box<dyn Action>,
+        tooltip: impl Into<SharedString>,
+        on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
+    ) -> Self {
+        Self {
+            id: id.into(),
+            icon,
+            toggled,
+            action,
+            tooltip: tooltip.into(),
+            on_click: Box::new(on_click),
+        }
+    }
+}
+
+impl RenderOnce for QuickActionBarButton {
+    fn render(self, _: &mut WindowContext) -> impl IntoElement {
+        let tooltip = self.tooltip.clone();
+        let action = self.action.boxed_clone();
+
+        IconButton::new(self.id.clone(), self.icon)
+            .size(ButtonSize::Compact)
+            .icon_size(IconSize::Small)
+            .style(ButtonStyle::Subtle)
+            .selected(self.toggled)
+            .tooltip(move |cx| Tooltip::for_action(tooltip.clone(), &*action, cx))
+            .on_click(move |event, cx| (self.on_click)(event, cx))
+    }
 }
 
 impl ToolbarItemView for QuickActionBar {
@@ -161,12 +159,12 @@ impl ToolbarItemView for QuickActionBar {
         match active_pane_item {
             Some(active_item) => {
                 self.active_item = Some(active_item.boxed_clone());
-                self.inlay_hints_enabled_subscription.take();
+                self._inlay_hints_enabled_subscription.take();
 
                 if let Some(editor) = active_item.downcast::<Editor>() {
                     let mut inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
                     let mut supports_inlay_hints = editor.read(cx).supports_inlay_hints(cx);
-                    self.inlay_hints_enabled_subscription =
+                    self._inlay_hints_enabled_subscription =
                         Some(cx.observe(&editor, move |_, editor, cx| {
                             let editor = editor.read(cx);
                             let new_inlay_hints_enabled = editor.inlay_hints_enabled();
@@ -179,7 +177,7 @@ impl ToolbarItemView for QuickActionBar {
                                 cx.notify()
                             }
                         }));
-                    ToolbarItemLocation::PrimaryRight { flex: None }
+                    ToolbarItemLocation::PrimaryRight
                 } else {
                     ToolbarItemLocation::Hidden
                 }

crates/quick_action_bar2/Cargo.toml 🔗

@@ -1,22 +0,0 @@
-[package]
-name = "quick_action_bar2"
-version = "0.1.0"
-edition = "2021"
-publish = false
-
-[lib]
-path = "src/quick_action_bar.rs"
-doctest = false
-
-[dependencies]
-assistant = { package = "assistant2", path = "../assistant2" }
-editor = { package = "editor2", path = "../editor2" }
-gpui = { package = "gpui2", path = "../gpui2" }
-search = { package = "search2", path = "../search2" }
-workspace = { package = "workspace2", path = "../workspace2" }
-ui = { package = "ui2", path = "../ui2" }
-
-[dev-dependencies]
-editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
-gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
-workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }

crates/quick_action_bar2/src/quick_action_bar.rs 🔗

@@ -1,191 +0,0 @@
-use assistant::{AssistantPanel, InlineAssist};
-use editor::Editor;
-
-use gpui::{
-    Action, ClickEvent, ElementId, EventEmitter, InteractiveElement, ParentElement, Render, Styled,
-    Subscription, View, ViewContext, WeakView,
-};
-use search::{buffer_search, BufferSearchBar};
-use ui::{prelude::*, ButtonSize, ButtonStyle, Icon, IconButton, IconSize, Tooltip};
-use workspace::{
-    item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
-};
-
-pub struct QuickActionBar {
-    buffer_search_bar: View<BufferSearchBar>,
-    active_item: Option<Box<dyn ItemHandle>>,
-    _inlay_hints_enabled_subscription: Option<Subscription>,
-    workspace: WeakView<Workspace>,
-}
-
-impl QuickActionBar {
-    pub fn new(buffer_search_bar: View<BufferSearchBar>, workspace: &Workspace) -> Self {
-        Self {
-            buffer_search_bar,
-            active_item: None,
-            _inlay_hints_enabled_subscription: None,
-            workspace: workspace.weak_handle(),
-        }
-    }
-
-    fn active_editor(&self) -> Option<View<Editor>> {
-        self.active_item
-            .as_ref()
-            .and_then(|item| item.downcast::<Editor>())
-    }
-}
-
-impl Render for QuickActionBar {
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
-        let Some(editor) = self.active_editor() else {
-            return div().id("empty quick action bar");
-        };
-
-        let inlay_hints_button = Some(QuickActionBarButton::new(
-            "toggle inlay hints",
-            Icon::InlayHint,
-            editor.read(cx).inlay_hints_enabled(),
-            Box::new(editor::ToggleInlayHints),
-            "Toggle Inlay Hints",
-            {
-                let editor = editor.clone();
-                move |_, cx| {
-                    editor.update(cx, |editor, cx| {
-                        editor.toggle_inlay_hints(&editor::ToggleInlayHints, cx);
-                    });
-                }
-            },
-        ))
-        .filter(|_| editor.read(cx).supports_inlay_hints(cx));
-
-        let search_button = Some(QuickActionBarButton::new(
-            "toggle buffer search",
-            Icon::MagnifyingGlass,
-            !self.buffer_search_bar.read(cx).is_dismissed(),
-            Box::new(buffer_search::Deploy { focus: false }),
-            "Buffer Search",
-            {
-                let buffer_search_bar = self.buffer_search_bar.clone();
-                move |_, cx| {
-                    buffer_search_bar.update(cx, |search_bar, cx| {
-                        search_bar.toggle(&buffer_search::Deploy { focus: true }, cx)
-                    });
-                }
-            },
-        ))
-        .filter(|_| editor.is_singleton(cx));
-
-        let assistant_button = QuickActionBarButton::new(
-            "toggle inline assistant",
-            Icon::MagicWand,
-            false,
-            Box::new(InlineAssist),
-            "Inline Assist",
-            {
-                let workspace = self.workspace.clone();
-                move |_, cx| {
-                    if let Some(workspace) = workspace.upgrade() {
-                        workspace.update(cx, |workspace, cx| {
-                            AssistantPanel::inline_assist(workspace, &InlineAssist, cx);
-                        });
-                    }
-                }
-            },
-        );
-
-        h_stack()
-            .id("quick action bar")
-            .p_1()
-            .gap_2()
-            .children(inlay_hints_button)
-            .children(search_button)
-            .child(assistant_button)
-    }
-}
-
-impl EventEmitter<ToolbarItemEvent> for QuickActionBar {}
-
-#[derive(IntoElement)]
-struct QuickActionBarButton {
-    id: ElementId,
-    icon: Icon,
-    toggled: bool,
-    action: Box<dyn Action>,
-    tooltip: SharedString,
-    on_click: Box<dyn Fn(&ClickEvent, &mut WindowContext)>,
-}
-
-impl QuickActionBarButton {
-    fn new(
-        id: impl Into<ElementId>,
-        icon: Icon,
-        toggled: bool,
-        action: Box<dyn Action>,
-        tooltip: impl Into<SharedString>,
-        on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
-    ) -> Self {
-        Self {
-            id: id.into(),
-            icon,
-            toggled,
-            action,
-            tooltip: tooltip.into(),
-            on_click: Box::new(on_click),
-        }
-    }
-}
-
-impl RenderOnce for QuickActionBarButton {
-    fn render(self, _: &mut WindowContext) -> impl IntoElement {
-        let tooltip = self.tooltip.clone();
-        let action = self.action.boxed_clone();
-
-        IconButton::new(self.id.clone(), self.icon)
-            .size(ButtonSize::Compact)
-            .icon_size(IconSize::Small)
-            .style(ButtonStyle::Subtle)
-            .selected(self.toggled)
-            .tooltip(move |cx| Tooltip::for_action(tooltip.clone(), &*action, cx))
-            .on_click(move |event, cx| (self.on_click)(event, cx))
-    }
-}
-
-impl ToolbarItemView for QuickActionBar {
-    fn set_active_pane_item(
-        &mut self,
-        active_pane_item: Option<&dyn ItemHandle>,
-        cx: &mut ViewContext<Self>,
-    ) -> ToolbarItemLocation {
-        match active_pane_item {
-            Some(active_item) => {
-                self.active_item = Some(active_item.boxed_clone());
-                self._inlay_hints_enabled_subscription.take();
-
-                if let Some(editor) = active_item.downcast::<Editor>() {
-                    let mut inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
-                    let mut supports_inlay_hints = editor.read(cx).supports_inlay_hints(cx);
-                    self._inlay_hints_enabled_subscription =
-                        Some(cx.observe(&editor, move |_, editor, cx| {
-                            let editor = editor.read(cx);
-                            let new_inlay_hints_enabled = editor.inlay_hints_enabled();
-                            let new_supports_inlay_hints = editor.supports_inlay_hints(cx);
-                            let should_notify = inlay_hints_enabled != new_inlay_hints_enabled
-                                || supports_inlay_hints != new_supports_inlay_hints;
-                            inlay_hints_enabled = new_inlay_hints_enabled;
-                            supports_inlay_hints = new_supports_inlay_hints;
-                            if should_notify {
-                                cx.notify()
-                            }
-                        }));
-                    ToolbarItemLocation::PrimaryRight
-                } else {
-                    ToolbarItemLocation::Hidden
-                }
-            }
-            None => {
-                self.active_item = None;
-                ToolbarItemLocation::Hidden
-            }
-        }
-    }
-}

crates/vcs_menu/Cargo.toml 🔗

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

crates/vcs_menu/src/lib.rs 🔗

@@ -2,57 +2,95 @@ use anyhow::{anyhow, bail, Result};
 use fs::repository::Branch;
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
-    actions,
-    elements::*,
-    platform::{CursorStyle, MouseButton},
-    AppContext, MouseState, Task, ViewContext, ViewHandle,
+    actions, rems, AnyElement, AppContext, DismissEvent, Element, EventEmitter, FocusHandle,
+    FocusableView, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled,
+    Subscription, Task, View, ViewContext, VisualContext, WindowContext,
 };
-use picker::{Picker, PickerDelegate, PickerEvent};
+use picker::{Picker, PickerDelegate};
 use std::{ops::Not, sync::Arc};
+use ui::{
+    h_stack, v_stack, Button, ButtonCommon, Clickable, HighlightedLabel, Label, LabelCommon,
+    LabelSize, ListItem, ListItemSpacing, Selectable,
+};
 use util::ResultExt;
-use workspace::{Toast, Workspace};
+use workspace::{ModalView, Toast, Workspace};
 
 actions!(branches, [OpenRecent]);
 
 pub fn init(cx: &mut AppContext) {
-    Picker::<BranchListDelegate>::init(cx);
-    cx.add_action(toggle);
+    // todo!() po
+    cx.observe_new_views(|workspace: &mut Workspace, _| {
+        workspace.register_action(|workspace, action, cx| {
+            BranchList::toggle_modal(workspace, action, cx).log_err();
+        });
+    })
+    .detach();
 }
-pub type BranchList = Picker<BranchListDelegate>;
 
-pub fn build_branch_list(
-    workspace: ViewHandle<Workspace>,
-    cx: &mut ViewContext<BranchList>,
-) -> Result<BranchList> {
-    let delegate = workspace.read_with(cx, |workspace, cx| {
-        BranchListDelegate::new(workspace, cx.handle(), 29, cx)
-    })?;
+pub struct BranchList {
+    pub picker: View<Picker<BranchListDelegate>>,
+    rem_width: f32,
+    _subscription: Subscription,
+}
+
+impl BranchList {
+    fn new(delegate: BranchListDelegate, rem_width: f32, cx: &mut ViewContext<Self>) -> Self {
+        let picker = cx.new_view(|cx| Picker::new(delegate, cx));
+        let _subscription = cx.subscribe(&picker, |_, _, _, cx| cx.emit(DismissEvent));
+        Self {
+            picker,
+            rem_width,
+            _subscription,
+        }
+    }
+    fn toggle_modal(
+        workspace: &mut Workspace,
+        _: &OpenRecent,
+        cx: &mut ViewContext<Workspace>,
+    ) -> Result<()> {
+        // Modal branch picker has a longer trailoff than a popover one.
+        let delegate = BranchListDelegate::new(workspace, cx.view().clone(), 70, cx)?;
+        workspace.toggle_modal(cx, |cx| BranchList::new(delegate, 34., cx));
 
-    Ok(Picker::new(delegate, cx).with_theme(|theme| theme.picker.clone()))
+        Ok(())
+    }
 }
+impl ModalView for BranchList {}
+impl EventEmitter<DismissEvent> for BranchList {}
 
-fn toggle(
-    workspace: &mut Workspace,
-    _: &OpenRecent,
-    cx: &mut ViewContext<Workspace>,
-) -> Result<()> {
-    // Modal branch picker has a longer trailoff than a popover one.
-    let delegate = BranchListDelegate::new(workspace, cx.handle(), 70, cx)?;
-    workspace.toggle_modal(cx, |_, cx| {
-        cx.add_view(|cx| {
-            Picker::new(delegate, cx)
-                .with_theme(|theme| theme.picker.clone())
-                .with_max_size(800., 1200.)
-        })
-    });
+impl FocusableView for BranchList {
+    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+        self.picker.focus_handle(cx)
+    }
+}
 
-    Ok(())
+impl Render for BranchList {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+        v_stack()
+            .w(rems(self.rem_width))
+            .child(self.picker.clone())
+            .on_mouse_down_out(cx.listener(|this, _, cx| {
+                this.picker.update(cx, |this, cx| {
+                    this.cancel(&Default::default(), cx);
+                })
+            }))
+    }
+}
+
+pub fn build_branch_list(
+    workspace: View<Workspace>,
+    cx: &mut WindowContext<'_>,
+) -> Result<View<BranchList>> {
+    let delegate = workspace.update(cx, |workspace, cx| {
+        BranchListDelegate::new(workspace, cx.view().clone(), 29, cx)
+    })?;
+    Ok(cx.new_view(move |cx| BranchList::new(delegate, 20., cx)))
 }
 
 pub struct BranchListDelegate {
     matches: Vec<StringMatch>,
     all_branches: Vec<Branch>,
-    workspace: ViewHandle<Workspace>,
+    workspace: View<Workspace>,
     selected_index: usize,
     last_query: String,
     /// Max length of branch name before we truncate it and add a trailing `...`.
@@ -62,7 +100,7 @@ pub struct BranchListDelegate {
 impl BranchListDelegate {
     fn new(
         workspace: &Workspace,
-        handle: ViewHandle<Workspace>,
+        handle: View<Workspace>,
         branch_name_trailoff_after: usize,
         cx: &AppContext,
     ) -> Result<Self> {
@@ -87,7 +125,7 @@ impl BranchListDelegate {
         })
     }
 
-    fn display_error_toast(&self, message: String, cx: &mut ViewContext<BranchList>) {
+    fn display_error_toast(&self, message: String, cx: &mut WindowContext<'_>) {
         const GIT_CHECKOUT_FAILURE_ID: usize = 2048;
         self.workspace.update(cx, |model, ctx| {
             model.show_toast(Toast::new(GIT_CHECKOUT_FAILURE_ID, message), ctx)
@@ -96,6 +134,8 @@ impl BranchListDelegate {
 }
 
 impl PickerDelegate for BranchListDelegate {
+    type ListItem = ListItem;
+
     fn placeholder_text(&self) -> Arc<str> {
         "Select branch...".into()
     }
@@ -114,9 +154,9 @@ impl PickerDelegate for BranchListDelegate {
 
     fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
         cx.spawn(move |picker, mut cx| async move {
-            let candidates = picker.read_with(&mut cx, |view, _| {
+            let candidates = picker.update(&mut cx, |view, _| {
                 const RECENT_BRANCHES_COUNT: usize = 10;
-                let mut branches = view.delegate().all_branches.clone();
+                let mut branches = view.delegate.all_branches.clone();
                 if query.is_empty() && branches.len() > RECENT_BRANCHES_COUNT {
                     // Truncate list of recent branches
                     // Do a partial sort to show recent-ish branches first.
@@ -157,13 +197,13 @@ impl PickerDelegate for BranchListDelegate {
                     true,
                     10000,
                     &Default::default(),
-                    cx.background(),
+                    cx.background_executor().clone(),
                 )
                 .await
             };
             picker
                 .update(&mut cx, |picker, _| {
-                    let delegate = picker.delegate_mut();
+                    let delegate = &mut picker.delegate;
                     delegate.matches = matches;
                     if delegate.matches.is_empty() {
                         delegate.selected_index = 0;
@@ -189,7 +229,7 @@ impl PickerDelegate for BranchListDelegate {
         cx.spawn(|picker, mut cx| async move {
             picker
                 .update(&mut cx, |this, cx| {
-                    let project = this.delegate().workspace.read(cx).project().read(cx);
+                    let project = this.delegate.workspace.read(cx).project().read(cx);
                     let mut cwd = project
                         .visible_worktrees(cx)
                         .next()
@@ -210,10 +250,10 @@ impl PickerDelegate for BranchListDelegate {
                         .lock()
                         .change_branch(&current_pick);
                     if status.is_err() {
-                        this.delegate().display_error_toast(format!("Failed to checkout branch '{current_pick}', check for conflicts or unstashed files"), cx);
+                        this.delegate.display_error_toast(format!("Failed to checkout branch '{current_pick}', check for conflicts or unstashed files"), cx);
                         status?;
                     }
-                    cx.emit(PickerEvent::Dismiss);
+                    cx.emit(DismissEvent);
 
                     Ok::<(), anyhow::Error>(())
                 })
@@ -223,123 +263,96 @@ impl PickerDelegate for BranchListDelegate {
     }
 
     fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
-        cx.emit(PickerEvent::Dismiss);
+        cx.emit(DismissEvent);
     }
 
     fn render_match(
         &self,
         ix: usize,
-        mouse_state: &mut MouseState,
         selected: bool,
-        cx: &gpui::AppContext,
-    ) -> AnyElement<Picker<Self>> {
-        let theme = &theme::current(cx);
+        _cx: &mut ViewContext<Picker<Self>>,
+    ) -> Option<Self::ListItem> {
         let hit = &self.matches[ix];
         let shortened_branch_name =
             util::truncate_and_trailoff(&hit.string, self.branch_name_trailoff_after);
-        let highlights = hit
+        let highlights: Vec<_> = hit
             .positions
             .iter()
+            .filter(|index| index < &&self.branch_name_trailoff_after)
             .copied()
-            .filter(|index| index < &self.branch_name_trailoff_after)
             .collect();
-        let style = theme.picker.item.in_state(selected).style_for(mouse_state);
-        Flex::row()
-            .with_child(
-                Label::new(shortened_branch_name.clone(), style.label.clone())
-                    .with_highlights(highlights)
-                    .contained()
-                    .aligned()
-                    .left(),
-            )
-            .contained()
-            .with_style(style.container)
-            .constrained()
-            .with_height(theme.collab_panel.tabbed_modal.row_height)
-            .into_any()
+        Some(
+            ListItem::new(SharedString::from(format!("vcs-menu-{ix}")))
+                .inset(true)
+                .spacing(ListItemSpacing::Sparse)
+                .selected(selected)
+                .start_slot(HighlightedLabel::new(shortened_branch_name, highlights)),
+        )
     }
-    fn render_header(
-        &self,
-        cx: &mut ViewContext<Picker<Self>>,
-    ) -> Option<AnyElement<Picker<Self>>> {
-        let theme = &theme::current(cx);
-        let style = theme.picker.header.clone();
+    fn render_header(&self, _: &mut ViewContext<Picker<Self>>) -> Option<AnyElement> {
         let label = if self.last_query.is_empty() {
-            Flex::row()
-                .with_child(Label::new("Recent branches", style.label.clone()))
-                .contained()
-                .with_style(style.container)
+            h_stack()
+                .ml_3()
+                .child(Label::new("Recent branches").size(LabelSize::Small))
         } else {
-            Flex::row()
-                .with_child(Label::new("Branches", style.label.clone()))
-                .with_children(self.matches.is_empty().not().then(|| {
-                    let suffix = if self.matches.len() == 1 { "" } else { "es" };
-                    Label::new(
-                        format!("{} match{}", self.matches.len(), suffix),
-                        style.label,
-                    )
-                    .flex_float()
-                }))
-                .contained()
-                .with_style(style.container)
+            let match_label = self.matches.is_empty().not().then(|| {
+                let suffix = if self.matches.len() == 1 { "" } else { "es" };
+                Label::new(format!("{} match{}", self.matches.len(), suffix)).size(LabelSize::Small)
+            });
+            h_stack()
+                .px_3()
+                .h_full()
+                .justify_between()
+                .child(Label::new("Branches").size(LabelSize::Small))
+                .children(match_label)
         };
         Some(label.into_any())
     }
-    fn render_footer(
-        &self,
-        cx: &mut ViewContext<Picker<Self>>,
-    ) -> Option<AnyElement<Picker<Self>>> {
-        if !self.last_query.is_empty() {
-            let theme = &theme::current(cx);
-            let style = theme.picker.footer.clone();
-            enum BranchCreateButton {}
-            Some(
-                Flex::row().with_child(MouseEventHandler::new::<BranchCreateButton, _>(0, cx, |state, _| {
-                    let style = style.style_for(state);
-                    Label::new("Create branch", style.label.clone())
-                        .contained()
-                        .with_style(style.container)
-                })
-                .with_cursor_style(CursorStyle::PointingHand)
-                .on_down(MouseButton::Left, |_, _, cx| {
-                    cx.spawn(|picker, mut cx| async move {
-                        picker.update(&mut cx, |this, cx| {
-                            let project = this.delegate().workspace.read(cx).project().read(cx);
-                            let current_pick = &this.delegate().last_query;
-                            let mut cwd = project
-                            .visible_worktrees(cx)
-                            .next()
-                            .ok_or_else(|| anyhow!("There are no visisible worktrees."))?
-                            .read(cx)
-                            .abs_path()
-                            .to_path_buf();
-                            cwd.push(".git");
-                            let repo = project
-                                .fs()
-                                .open_repo(&cwd)
-                                .ok_or_else(|| anyhow!("Could not open repository at path `{}`", cwd.as_os_str().to_string_lossy()))?;
-                            let repo = repo
-                                .lock();
-                            let status = repo
-                                .create_branch(&current_pick);
-                            if status.is_err() {
-                                this.delegate().display_error_toast(format!("Failed to create branch '{current_pick}', check for conflicts or unstashed files"), cx);
-                                status?;
-                            }
-                            let status = repo.change_branch(&current_pick);
-                            if status.is_err() {
-                                this.delegate().display_error_toast(format!("Failed to chec branch '{current_pick}', check for conflicts or unstashed files"), cx);
-                                status?;
-                            }
-                            cx.emit(PickerEvent::Dismiss);
-                            Ok::<(), anyhow::Error>(())
-                })
-                    }).detach();
-                })).aligned().right()
-                .into_any(),
-            )
-        } else {
-            None
+    fn render_footer(&self, cx: &mut ViewContext<Picker<Self>>) -> Option<AnyElement> {
+        if self.last_query.is_empty() {
+            return None;
         }
+
+        Some(
+            h_stack().mr_3().pb_2().child(h_stack().w_full()).child(
+            Button::new("branch-picker-create-branch-button", "Create branch").on_click(
+                cx.listener(|_, _, cx| {
+                    cx.spawn(|picker, mut cx| async move {
+                                        picker.update(&mut cx, |this, cx| {
+                                            let project = this.delegate.workspace.read(cx).project().read(cx);
+                                            let current_pick = &this.delegate.last_query;
+                                            let mut cwd = project
+                                            .visible_worktrees(cx)
+                                            .next()
+                                            .ok_or_else(|| anyhow!("There are no visisible worktrees."))?
+                                            .read(cx)
+                                            .abs_path()
+                                            .to_path_buf();
+                                            cwd.push(".git");
+                                            let repo = project
+                                                .fs()
+                                                .open_repo(&cwd)
+                                                .ok_or_else(|| anyhow!("Could not open repository at path `{}`", cwd.as_os_str().to_string_lossy()))?;
+                                            let repo = repo
+                                                .lock();
+                                            let status = repo
+                                                .create_branch(&current_pick);
+                                            if status.is_err() {
+                                                this.delegate.display_error_toast(format!("Failed to create branch '{current_pick}', check for conflicts or unstashed files"), cx);
+                                                status?;
+                                            }
+                                            let status = repo.change_branch(&current_pick);
+                                            if status.is_err() {
+                                                this.delegate.display_error_toast(format!("Failed to chec branch '{current_pick}', check for conflicts or unstashed files"), cx);
+                                                status?;
+                                            }
+                                            this.cancel(&Default::default(), cx);
+                                            Ok::<(), anyhow::Error>(())
+                                })
+
+                    }).detach_and_log_err(cx);
+                }),
+            ).style(ui::ButtonStyle::Filled)).into_any_element(),
+        )
     }
 }

crates/vcs_menu2/Cargo.toml 🔗

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

crates/vcs_menu2/src/lib.rs 🔗

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

crates/welcome/Cargo.toml 🔗

@@ -11,21 +11,22 @@ path = "src/welcome.rs"
 test-support = []
 
 [dependencies]
-client = { path = "../client" }
-editor = { path = "../editor" }
-fs = { path = "../fs" }
-fuzzy = { path = "../fuzzy" }
-gpui = { path = "../gpui" }
-db = { path = "../db" }
-install_cli = { path = "../install_cli" }
-project = { path = "../project" }
-settings = { path = "../settings" }
-theme = { path = "../theme" }
-theme_selector = { path = "../theme_selector" }
+client = { package = "client2", path = "../client2" }
+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" }
+settings = { package = "settings2", path = "../settings2" }
+theme = { package = "theme2", path = "../theme2" }
+theme_selector = { package = "theme_selector2", path = "../theme_selector2" }
 util = { path = "../util" }
-picker = { path = "../picker" }
-workspace = { path = "../workspace" }
-vim = { path = "../vim" }
+picker = { package = "picker2", path = "../picker2" }
+workspace = { package = "workspace2", path = "../workspace2" }
+vim = { package = "vim2", path = "../vim2" }
 
 anyhow.workspace = true
 log.workspace = true
@@ -33,4 +34,4 @@ schemars.workspace = true
 serde.workspace = true
 
 [dev-dependencies]
-editor = { path = "../editor", features = ["test-support"] }
+editor = { package = "editor2", path = "../editor2", features = ["test-support"] }

crates/welcome/src/base_keymap_picker.rs 🔗

@@ -1,22 +1,24 @@
 use super::base_keymap_setting::BaseKeymap;
 use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
 use gpui::{
-    actions,
-    elements::{Element as _, Label},
-    AppContext, Task, ViewContext,
+    actions, AppContext, DismissEvent, EventEmitter, FocusableView, Render, Task, View,
+    ViewContext, VisualContext, WeakView,
 };
-use picker::{Picker, PickerDelegate, PickerEvent};
+use picker::{Picker, PickerDelegate};
 use project::Fs;
-use settings::update_settings_file;
+use settings::{update_settings_file, Settings};
 use std::sync::Arc;
+use ui::{prelude::*, ListItem, ListItemSpacing};
 use util::ResultExt;
-use workspace::Workspace;
+use workspace::{ui::HighlightedLabel, ModalView, Workspace};
 
 actions!(welcome, [ToggleBaseKeymapSelector]);
 
 pub fn init(cx: &mut AppContext) {
-    cx.add_action(toggle);
-    BaseKeymapSelector::init(cx);
+    cx.observe_new_views(|workspace: &mut Workspace, _cx| {
+        workspace.register_action(toggle);
+    })
+    .detach();
 }
 
 pub fn toggle(
@@ -24,28 +26,69 @@ pub fn toggle(
     _: &ToggleBaseKeymapSelector,
     cx: &mut ViewContext<Workspace>,
 ) {
-    workspace.toggle_modal(cx, |workspace, cx| {
-        let fs = workspace.app_state().fs.clone();
-        cx.add_view(|cx| BaseKeymapSelector::new(BaseKeymapSelectorDelegate::new(fs, cx), cx))
+    let fs = workspace.app_state().fs.clone();
+    workspace.toggle_modal(cx, |cx| {
+        BaseKeymapSelector::new(
+            BaseKeymapSelectorDelegate::new(cx.view().downgrade(), fs, cx),
+            cx,
+        )
     });
 }
 
-pub type BaseKeymapSelector = Picker<BaseKeymapSelectorDelegate>;
+pub struct BaseKeymapSelector {
+    focus_handle: gpui::FocusHandle,
+    picker: View<Picker<BaseKeymapSelectorDelegate>>,
+}
+
+impl FocusableView for BaseKeymapSelector {
+    fn focus_handle(&self, _cx: &AppContext) -> gpui::FocusHandle {
+        self.focus_handle.clone()
+    }
+}
+
+impl EventEmitter<DismissEvent> for BaseKeymapSelector {}
+impl ModalView for BaseKeymapSelector {}
+
+impl BaseKeymapSelector {
+    pub fn new(
+        delegate: BaseKeymapSelectorDelegate,
+        cx: &mut ViewContext<BaseKeymapSelector>,
+    ) -> Self {
+        let picker = cx.new_view(|cx| Picker::new(delegate, cx));
+        let focus_handle = cx.focus_handle();
+        Self {
+            focus_handle,
+            picker,
+        }
+    }
+}
+
+impl Render for BaseKeymapSelector {
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
+        self.picker.clone()
+    }
+}
 
 pub struct BaseKeymapSelectorDelegate {
+    view: WeakView<BaseKeymapSelector>,
     matches: Vec<StringMatch>,
     selected_index: usize,
     fs: Arc<dyn Fs>,
 }
 
 impl BaseKeymapSelectorDelegate {
-    fn new(fs: Arc<dyn Fs>, cx: &mut ViewContext<BaseKeymapSelector>) -> Self {
-        let base = settings::get::<BaseKeymap>(cx);
+    fn new(
+        weak_view: WeakView<BaseKeymapSelector>,
+        fs: Arc<dyn Fs>,
+        cx: &mut ViewContext<BaseKeymapSelector>,
+    ) -> Self {
+        let base = BaseKeymap::get(None, cx);
         let selected_index = BaseKeymap::OPTIONS
             .iter()
             .position(|(_, value)| value == base)
             .unwrap_or(0);
         Self {
+            view: weak_view,
             matches: Vec::new(),
             selected_index,
             fs,
@@ -54,6 +97,8 @@ impl BaseKeymapSelectorDelegate {
 }
 
 impl PickerDelegate for BaseKeymapSelectorDelegate {
+    type ListItem = ui::ListItem;
+
     fn placeholder_text(&self) -> Arc<str> {
         "Select a base keymap...".into()
     }
@@ -66,16 +111,20 @@ impl PickerDelegate for BaseKeymapSelectorDelegate {
         self.selected_index
     }
 
-    fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<BaseKeymapSelector>) {
+    fn set_selected_index(
+        &mut self,
+        ix: usize,
+        _: &mut ViewContext<Picker<BaseKeymapSelectorDelegate>>,
+    ) {
         self.selected_index = ix;
     }
 
     fn update_matches(
         &mut self,
         query: String,
-        cx: &mut ViewContext<BaseKeymapSelector>,
+        cx: &mut ViewContext<Picker<BaseKeymapSelectorDelegate>>,
     ) -> Task<()> {
-        let background = cx.background().clone();
+        let background = cx.background_executor().clone();
         let candidates = BaseKeymap::names()
             .enumerate()
             .map(|(id, name)| StringMatchCandidate {
@@ -110,43 +159,50 @@ impl PickerDelegate for BaseKeymapSelectorDelegate {
             };
 
             this.update(&mut cx, |this, _| {
-                let delegate = this.delegate_mut();
-                delegate.matches = matches;
-                delegate.selected_index = delegate
+                this.delegate.matches = matches;
+                this.delegate.selected_index = this
+                    .delegate
                     .selected_index
-                    .min(delegate.matches.len().saturating_sub(1));
+                    .min(this.delegate.matches.len().saturating_sub(1));
             })
             .log_err();
         })
     }
 
-    fn confirm(&mut self, _: bool, cx: &mut ViewContext<BaseKeymapSelector>) {
+    fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<BaseKeymapSelectorDelegate>>) {
         if let Some(selection) = self.matches.get(self.selected_index) {
             let base_keymap = BaseKeymap::from_names(&selection.string);
             update_settings_file::<BaseKeymap>(self.fs.clone(), cx, move |setting| {
                 *setting = Some(base_keymap)
             });
         }
-        cx.emit(PickerEvent::Dismiss);
+
+        self.view
+            .update(cx, |_, cx| {
+                cx.emit(DismissEvent);
+            })
+            .ok();
     }
 
-    fn dismissed(&mut self, _cx: &mut ViewContext<BaseKeymapSelector>) {}
+    fn dismissed(&mut self, _cx: &mut ViewContext<Picker<BaseKeymapSelectorDelegate>>) {}
 
     fn render_match(
         &self,
         ix: usize,
-        mouse_state: &mut gpui::MouseState,
         selected: bool,
-        cx: &gpui::AppContext,
-    ) -> gpui::AnyElement<Picker<Self>> {
-        let theme = &theme::current(cx);
+        _cx: &mut gpui::ViewContext<Picker<Self>>,
+    ) -> Option<Self::ListItem> {
         let keymap_match = &self.matches[ix];
-        let style = theme.picker.item.in_state(selected).style_for(mouse_state);
 
-        Label::new(keymap_match.string.clone(), style.label.clone())
-            .with_highlights(keymap_match.positions.clone())
-            .contained()
-            .with_style(style.container)
-            .into_any()
+        Some(
+            ListItem::new(ix)
+                .inset(true)
+                .spacing(ListItemSpacing::Sparse)
+                .selected(selected)
+                .child(HighlightedLabel::new(
+                    keymap_match.string.clone(),
+                    keymap_match.positions.clone(),
+                )),
+        )
     }
 }

crates/welcome/src/base_keymap_setting.rs 🔗

@@ -1,6 +1,6 @@
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::Setting;
+use settings::Settings;
 
 #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
 pub enum BaseKeymap {
@@ -44,7 +44,7 @@ impl BaseKeymap {
     }
 }
 
-impl Setting for BaseKeymap {
+impl Settings for BaseKeymap {
     const KEY: Option<&'static str> = Some("base_keymap");
 
     type FileContent = Option<Self>;
@@ -52,7 +52,7 @@ impl Setting for BaseKeymap {
     fn load(
         default_value: &Self::FileContent,
         user_values: &[&Self::FileContent],
-        _: &gpui::AppContext,
+        _: &mut gpui::AppContext,
     ) -> anyhow::Result<Self>
     where
         Self: Sized,

crates/welcome/src/welcome.rs 🔗

@@ -1,19 +1,21 @@
 mod base_keymap_picker;
 mod base_keymap_setting;
 
-use crate::base_keymap_picker::ToggleBaseKeymapSelector;
 use client::TelemetrySettings;
 use db::kvp::KEY_VALUE_STORE;
 use gpui::{
-    elements::{Flex, Label, ParentElement},
-    AnyElement, AppContext, Element, Entity, Subscription, View, ViewContext, WeakViewHandle,
+    svg, AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView, InteractiveElement,
+    ParentElement, Render, Styled, Subscription, View, ViewContext, VisualContext, WeakView,
+    WindowContext,
 };
-use settings::{update_settings_file, SettingsStore};
-use std::{borrow::Cow, sync::Arc};
+use settings::{Settings, SettingsStore};
+use std::sync::Arc;
+use ui::{prelude::*, Checkbox};
 use vim::VimModeSetting;
 use workspace::{
-    dock::DockPosition, item::Item, open_new, AppState, PaneBackdrop, Welcome, Workspace,
-    WorkspaceId,
+    dock::DockPosition,
+    item::{Item, ItemEvent},
+    open_new, AppState, Welcome, Workspace, WorkspaceId,
 };
 
 pub use base_keymap_setting::BaseKeymap;
@@ -21,22 +23,25 @@ pub use base_keymap_setting::BaseKeymap;
 pub const FIRST_OPEN: &str = "first_open";
 
 pub fn init(cx: &mut AppContext) {
-    settings::register::<BaseKeymap>(cx);
+    BaseKeymap::register(cx);
 
-    cx.add_action(|workspace: &mut Workspace, _: &Welcome, cx| {
-        let welcome_page = cx.add_view(|cx| WelcomePage::new(workspace, cx));
-        workspace.add_item(Box::new(welcome_page), cx)
-    });
+    cx.observe_new_views(|workspace: &mut Workspace, _cx| {
+        workspace.register_action(|workspace, _: &Welcome, cx| {
+            let welcome_page = cx.new_view(|cx| WelcomePage::new(workspace, cx));
+            workspace.add_item(Box::new(welcome_page), cx)
+        });
+    })
+    .detach();
 
     base_keymap_picker::init(cx);
 }
 
-pub fn show_welcome_experience(app_state: &Arc<AppState>, cx: &mut AppContext) {
+pub fn show_welcome_view(app_state: &Arc<AppState>, cx: &mut AppContext) {
     open_new(&app_state, cx, |workspace, cx| {
         workspace.toggle_dock(DockPosition::Left, cx);
-        let welcome_page = cx.add_view(|cx| WelcomePage::new(workspace, cx));
+        let welcome_page = cx.new_view(|cx| WelcomePage::new(workspace, cx));
         workspace.add_item_to_center(Box::new(welcome_page.clone()), cx);
-        cx.focus(&welcome_page);
+        cx.focus_view(&welcome_page);
         cx.notify();
     })
     .detach();
@@ -47,227 +52,213 @@ pub fn show_welcome_experience(app_state: &Arc<AppState>, cx: &mut AppContext) {
 }
 
 pub struct WelcomePage {
-    workspace: WeakViewHandle<Workspace>,
+    workspace: WeakView<Workspace>,
+    focus_handle: FocusHandle,
     _settings_subscription: Subscription,
 }
 
-impl Entity for WelcomePage {
-    type Event = ();
-}
-
-impl View for WelcomePage {
-    fn ui_name() -> &'static str {
-        "WelcomePage"
-    }
-
-    fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> AnyElement<Self> {
-        let self_handle = cx.handle();
-        let theme = theme::current(cx);
-        let width = theme.welcome.page_width;
-
-        let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
-        let vim_mode_setting = settings::get::<VimModeSetting>(cx).0;
-
-        enum Metrics {}
-        enum Diagnostics {}
-
-        PaneBackdrop::new(
-            self_handle.id(),
-            Flex::column()
-                .with_child(
-                    Flex::column()
-                        .with_child(
-                            theme::ui::svg(&theme.welcome.logo)
-                                .aligned()
-                                .contained()
-                                .aligned(),
-                        )
-                        .with_child(
-                            Label::new(
-                                "Code at the speed of thought",
-                                theme.welcome.logo_subheading.text.clone(),
-                            )
-                            .aligned()
-                            .contained()
-                            .with_style(theme.welcome.logo_subheading.container),
-                        )
-                        .contained()
-                        .with_style(theme.welcome.heading_group)
-                        .constrained()
-                        .with_width(width),
+impl Render for WelcomePage {
+    fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
+        h_stack().full().track_focus(&self.focus_handle).child(
+            v_stack()
+                .w_96()
+                .gap_4()
+                .mx_auto()
+                .child(
+                    svg()
+                        .path("icons/logo_96.svg")
+                        .text_color(gpui::white())
+                        .w(px(96.))
+                        .h(px(96.))
+                        .mx_auto(),
                 )
-                .with_child(
-                    Flex::column()
-                        .with_child(theme::ui::cta_button::<theme_selector::Toggle, _, _, _>(
-                            "Choose a theme",
-                            width,
-                            &theme.welcome.button,
-                            cx,
-                            |_, this, cx| {
-                                if let Some(workspace) = this.workspace.upgrade(cx) {
-                                    workspace.update(cx, |workspace, cx| {
-                                        theme_selector::toggle(workspace, &Default::default(), cx)
-                                    })
-                                }
-                            },
-                        ))
-                        .with_child(theme::ui::cta_button::<ToggleBaseKeymapSelector, _, _, _>(
-                            "Choose a keymap",
-                            width,
-                            &theme.welcome.button,
-                            cx,
-                            |_, this, cx| {
-                                if let Some(workspace) = this.workspace.upgrade(cx) {
-                                    workspace.update(cx, |workspace, cx| {
-                                        base_keymap_picker::toggle(
-                                            workspace,
-                                            &Default::default(),
-                                            cx,
-                                        )
-                                    })
-                                }
-                            },
-                        ))
-                        .with_child(theme::ui::cta_button::<install_cli::Install, _, _, _>(
-                            "Install the CLI",
-                            width,
-                            &theme.welcome.button,
-                            cx,
-                            |_, _, cx| {
-                                cx.app_context()
-                                    .spawn(|cx| async move { install_cli::install_cli(&cx).await })
-                                    .detach_and_log_err(cx);
-                            },
-                        ))
-                        .contained()
-                        .with_style(theme.welcome.button_group)
-                        .constrained()
-                        .with_width(width),
+                .child(
+                    h_stack()
+                        .justify_center()
+                        .child(Label::new("Code at the speed of thought")),
                 )
-                .with_child(
-                    Flex::column()
-                        .with_child(
-                            theme::ui::checkbox::<Diagnostics, Self, _>(
-                                "Enable vim mode",
-                                &theme.welcome.checkbox,
-                                vim_mode_setting,
-                                0,
-                                cx,
-                                |this, checked, cx| {
-                                    if let Some(workspace) = this.workspace.upgrade(cx) {
-                                        let fs = workspace.read(cx).app_state().fs.clone();
-                                        update_settings_file::<VimModeSetting>(
-                                            fs,
-                                            cx,
-                                            move |setting| *setting = Some(checked),
-                                        )
-                                    }
-                                },
-                            )
-                            .contained()
-                            .with_style(theme.welcome.checkbox_container),
+                .child(
+                    v_stack()
+                        .gap_2()
+                        .child(
+                            Button::new("choose-theme", "Choose a theme")
+                                .full_width()
+                                .on_click(cx.listener(|this, _, cx| {
+                                    this.workspace
+                                        .update(cx, |workspace, cx| {
+                                            theme_selector::toggle(
+                                                workspace,
+                                                &Default::default(),
+                                                cx,
+                                            )
+                                        })
+                                        .ok();
+                                })),
                         )
-                        .with_child(
-                            theme::ui::checkbox_with_label::<Metrics, _, Self, _>(
-                                Flex::column()
-                                    .with_child(
-                                        Label::new(
-                                            "Send anonymous usage data",
-                                            theme.welcome.checkbox.label.text.clone(),
+                        .child(
+                            Button::new("choose-keymap", "Choose a keymap")
+                                .full_width()
+                                .on_click(cx.listener(|this, _, cx| {
+                                    this.workspace
+                                        .update(cx, |workspace, cx| {
+                                            base_keymap_picker::toggle(
+                                                workspace,
+                                                &Default::default(),
+                                                cx,
+                                            )
+                                        })
+                                        .ok();
+                                })),
+                        )
+                        .child(
+                            Button::new("install-cli", "Install the CLI")
+                                .full_width()
+                                .on_click(cx.listener(|_, _, cx| {
+                                    cx.app_mut()
+                                        .spawn(
+                                            |cx| async move { install_cli::install_cli(&cx).await },
                                         )
-                                        .contained()
-                                        .with_style(theme.welcome.checkbox.label.container),
+                                        .detach_and_log_err(cx);
+                                })),
+                        ),
+                )
+                .child(
+                    v_stack()
+                        .p_3()
+                        .gap_2()
+                        .bg(cx.theme().colors().elevated_surface_background)
+                        .border_1()
+                        .border_color(cx.theme().colors().border)
+                        .rounded_md()
+                        .child(
+                            h_stack()
+                                .gap_2()
+                                .child(
+                                    Checkbox::new(
+                                        "enable-vim",
+                                        if VimModeSetting::get_global(cx).0 {
+                                            ui::Selection::Selected
+                                        } else {
+                                            ui::Selection::Unselected
+                                        },
                                     )
-                                    .with_child(
-                                        Label::new(
-                                            "Help > View Telemetry",
-                                            theme.welcome.usage_note.text.clone(),
-                                        )
-                                        .contained()
-                                        .with_style(theme.welcome.usage_note.container),
-                                    ),
-                                &theme.welcome.checkbox,
-                                telemetry_settings.metrics,
-                                0,
-                                cx,
-                                |this, checked, cx| {
-                                    if let Some(workspace) = this.workspace.upgrade(cx) {
-                                        let fs = workspace.read(cx).app_state().fs.clone();
-                                        update_settings_file::<TelemetrySettings>(
-                                            fs,
-                                            cx,
-                                            move |setting| setting.metrics = Some(checked),
-                                        )
-                                    }
-                                },
-                            )
-                            .contained()
-                            .with_style(theme.welcome.checkbox_container),
+                                    .on_click(cx.listener(
+                                        move |this, selection, cx| {
+                                            this.update_settings::<VimModeSetting>(
+                                                selection,
+                                                cx,
+                                                |setting, value| *setting = Some(value),
+                                            );
+                                        },
+                                    )),
+                                )
+                                .child(Label::new("Enable vim mode")),
                         )
-                        .with_child(
-                            theme::ui::checkbox::<Diagnostics, Self, _>(
-                                "Send crash reports",
-                                &theme.welcome.checkbox,
-                                telemetry_settings.diagnostics,
-                                1,
-                                cx,
-                                |this, checked, cx| {
-                                    if let Some(workspace) = this.workspace.upgrade(cx) {
-                                        let fs = workspace.read(cx).app_state().fs.clone();
-                                        update_settings_file::<TelemetrySettings>(
-                                            fs,
-                                            cx,
-                                            move |setting| setting.diagnostics = Some(checked),
-                                        )
-                                    }
-                                },
-                            )
-                            .contained()
-                            .with_style(theme.welcome.checkbox_container),
+                        .child(
+                            h_stack()
+                                .gap_2()
+                                .child(
+                                    Checkbox::new(
+                                        "enable-telemetry",
+                                        if TelemetrySettings::get_global(cx).metrics {
+                                            ui::Selection::Selected
+                                        } else {
+                                            ui::Selection::Unselected
+                                        },
+                                    )
+                                    .on_click(cx.listener(
+                                        move |this, selection, cx| {
+                                            this.update_settings::<TelemetrySettings>(
+                                                selection,
+                                                cx,
+                                                |settings, value| settings.metrics = Some(value),
+                                            );
+                                        },
+                                    )),
+                                )
+                                .child(Label::new("Send anonymous usage data")),
                         )
-                        .contained()
-                        .with_style(theme.welcome.checkbox_group)
-                        .constrained()
-                        .with_width(width),
-                )
-                .constrained()
-                .with_max_width(width)
-                .contained()
-                .with_uniform_padding(10.)
-                .aligned()
-                .into_any(),
+                        .child(
+                            h_stack()
+                                .gap_2()
+                                .child(
+                                    Checkbox::new(
+                                        "enable-crash",
+                                        if TelemetrySettings::get_global(cx).diagnostics {
+                                            ui::Selection::Selected
+                                        } else {
+                                            ui::Selection::Unselected
+                                        },
+                                    )
+                                    .on_click(cx.listener(
+                                        move |this, selection, cx| {
+                                            this.update_settings::<TelemetrySettings>(
+                                                selection,
+                                                cx,
+                                                |settings, value| {
+                                                    settings.diagnostics = Some(value)
+                                                },
+                                            );
+                                        },
+                                    )),
+                                )
+                                .child(Label::new("Send crash reports")),
+                        ),
+                ),
         )
-        .into_any_named("welcome page")
     }
 }
 
 impl WelcomePage {
     pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
         WelcomePage {
+            focus_handle: cx.focus_handle(),
             workspace: workspace.weak_handle(),
-            _settings_subscription: cx.observe_global::<SettingsStore, _>(move |_, cx| cx.notify()),
+            _settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
+        }
+    }
+
+    fn update_settings<T: Settings>(
+        &mut self,
+        selection: &Selection,
+        cx: &mut ViewContext<Self>,
+        callback: impl 'static + Send + Fn(&mut T::FileContent, bool),
+    ) {
+        if let Some(workspace) = self.workspace.upgrade() {
+            let fs = workspace.read(cx).app_state().fs.clone();
+            let selection = *selection;
+            settings::update_settings_file::<T>(fs, cx, move |settings| {
+                let value = match selection {
+                    Selection::Unselected => false,
+                    Selection::Selected => true,
+                    _ => return,
+                };
+
+                callback(settings, value)
+            });
         }
     }
 }
 
-impl Item for WelcomePage {
-    fn tab_tooltip_text(&self, _: &AppContext) -> Option<Cow<str>> {
-        Some("Welcome to Zed!".into())
+impl EventEmitter<ItemEvent> for WelcomePage {}
+
+impl FocusableView for WelcomePage {
+    fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
+        self.focus_handle.clone()
     }
+}
 
-    fn tab_content<T: 'static>(
-        &self,
-        _detail: Option<usize>,
-        style: &theme::Tab,
-        _cx: &gpui::AppContext,
-    ) -> AnyElement<T> {
-        Flex::row()
-            .with_child(
-                Label::new("Welcome to Zed!", style.label.clone())
-                    .aligned()
-                    .contained(),
-            )
-            .into_any()
+impl Item for WelcomePage {
+    type Event = ItemEvent;
+
+    fn tab_content(&self, _: Option<usize>, selected: bool, _: &WindowContext) -> AnyElement {
+        Label::new("Welcome to Zed!")
+            .color(if selected {
+                Color::Default
+            } else {
+                Color::Muted
+            })
+            .into_any_element()
     }
 
     fn show_toolbar(&self) -> bool {
@@ -278,10 +269,15 @@ impl Item for WelcomePage {
         &self,
         _workspace_id: WorkspaceId,
         cx: &mut ViewContext<Self>,
-    ) -> Option<Self> {
-        Some(WelcomePage {
+    ) -> Option<View<Self>> {
+        Some(cx.new_view(|cx| WelcomePage {
+            focus_handle: cx.focus_handle(),
             workspace: self.workspace.clone(),
-            _settings_subscription: cx.observe_global::<SettingsStore, _>(move |_, cx| cx.notify()),
-        })
+            _settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
+        }))
+    }
+
+    fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
+        f(*event)
     }
 }

crates/welcome2/Cargo.toml 🔗

@@ -1,37 +0,0 @@
-[package]
-name = "welcome2"
-version = "0.1.0"
-edition = "2021"
-publish = false
-
-[lib]
-path = "src/welcome.rs"
-
-[features]
-test-support = []
-
-[dependencies]
-client = { package = "client2", path = "../client2" }
-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" }
-settings = { package = "settings2", path = "../settings2" }
-theme = { package = "theme2", path = "../theme2" }
-theme_selector = { package = "theme_selector2", path = "../theme_selector2" }
-util = { path = "../util" }
-picker = { package = "picker2", path = "../picker2" }
-workspace = { package = "workspace2", path = "../workspace2" }
-vim = { package = "vim2", path = "../vim2" }
-
-anyhow.workspace = true
-log.workspace = true
-schemars.workspace = true
-serde.workspace = true
-
-[dev-dependencies]
-editor = { package = "editor2", path = "../editor2", features = ["test-support"] }

crates/welcome2/src/base_keymap_picker.rs 🔗

@@ -1,208 +0,0 @@
-use super::base_keymap_setting::BaseKeymap;
-use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
-use gpui::{
-    actions, AppContext, DismissEvent, EventEmitter, FocusableView, Render, Task, View,
-    ViewContext, VisualContext, WeakView,
-};
-use picker::{Picker, PickerDelegate};
-use project::Fs;
-use settings::{update_settings_file, Settings};
-use std::sync::Arc;
-use ui::{prelude::*, ListItem, ListItemSpacing};
-use util::ResultExt;
-use workspace::{ui::HighlightedLabel, ModalView, Workspace};
-
-actions!(welcome, [ToggleBaseKeymapSelector]);
-
-pub fn init(cx: &mut AppContext) {
-    cx.observe_new_views(|workspace: &mut Workspace, _cx| {
-        workspace.register_action(toggle);
-    })
-    .detach();
-}
-
-pub fn toggle(
-    workspace: &mut Workspace,
-    _: &ToggleBaseKeymapSelector,
-    cx: &mut ViewContext<Workspace>,
-) {
-    let fs = workspace.app_state().fs.clone();
-    workspace.toggle_modal(cx, |cx| {
-        BaseKeymapSelector::new(
-            BaseKeymapSelectorDelegate::new(cx.view().downgrade(), fs, cx),
-            cx,
-        )
-    });
-}
-
-pub struct BaseKeymapSelector {
-    focus_handle: gpui::FocusHandle,
-    picker: View<Picker<BaseKeymapSelectorDelegate>>,
-}
-
-impl FocusableView for BaseKeymapSelector {
-    fn focus_handle(&self, _cx: &AppContext) -> gpui::FocusHandle {
-        self.focus_handle.clone()
-    }
-}
-
-impl EventEmitter<DismissEvent> for BaseKeymapSelector {}
-impl ModalView for BaseKeymapSelector {}
-
-impl BaseKeymapSelector {
-    pub fn new(
-        delegate: BaseKeymapSelectorDelegate,
-        cx: &mut ViewContext<BaseKeymapSelector>,
-    ) -> Self {
-        let picker = cx.new_view(|cx| Picker::new(delegate, cx));
-        let focus_handle = cx.focus_handle();
-        Self {
-            focus_handle,
-            picker,
-        }
-    }
-}
-
-impl Render for BaseKeymapSelector {
-    fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
-        self.picker.clone()
-    }
-}
-
-pub struct BaseKeymapSelectorDelegate {
-    view: WeakView<BaseKeymapSelector>,
-    matches: Vec<StringMatch>,
-    selected_index: usize,
-    fs: Arc<dyn Fs>,
-}
-
-impl BaseKeymapSelectorDelegate {
-    fn new(
-        weak_view: WeakView<BaseKeymapSelector>,
-        fs: Arc<dyn Fs>,
-        cx: &mut ViewContext<BaseKeymapSelector>,
-    ) -> Self {
-        let base = BaseKeymap::get(None, cx);
-        let selected_index = BaseKeymap::OPTIONS
-            .iter()
-            .position(|(_, value)| value == base)
-            .unwrap_or(0);
-        Self {
-            view: weak_view,
-            matches: Vec::new(),
-            selected_index,
-            fs,
-        }
-    }
-}
-
-impl PickerDelegate for BaseKeymapSelectorDelegate {
-    type ListItem = ui::ListItem;
-
-    fn placeholder_text(&self) -> Arc<str> {
-        "Select a base keymap...".into()
-    }
-
-    fn match_count(&self) -> usize {
-        self.matches.len()
-    }
-
-    fn selected_index(&self) -> usize {
-        self.selected_index
-    }
-
-    fn set_selected_index(
-        &mut self,
-        ix: usize,
-        _: &mut ViewContext<Picker<BaseKeymapSelectorDelegate>>,
-    ) {
-        self.selected_index = ix;
-    }
-
-    fn update_matches(
-        &mut self,
-        query: String,
-        cx: &mut ViewContext<Picker<BaseKeymapSelectorDelegate>>,
-    ) -> Task<()> {
-        let background = cx.background_executor().clone();
-        let candidates = BaseKeymap::names()
-            .enumerate()
-            .map(|(id, name)| StringMatchCandidate {
-                id,
-                char_bag: name.into(),
-                string: name.into(),
-            })
-            .collect::<Vec<_>>();
-
-        cx.spawn(|this, mut cx| async move {
-            let matches = if query.is_empty() {
-                candidates
-                    .into_iter()
-                    .enumerate()
-                    .map(|(index, candidate)| StringMatch {
-                        candidate_id: index,
-                        string: candidate.string,
-                        positions: Vec::new(),
-                        score: 0.0,
-                    })
-                    .collect()
-            } else {
-                match_strings(
-                    &candidates,
-                    &query,
-                    false,
-                    100,
-                    &Default::default(),
-                    background,
-                )
-                .await
-            };
-
-            this.update(&mut cx, |this, _| {
-                this.delegate.matches = matches;
-                this.delegate.selected_index = this
-                    .delegate
-                    .selected_index
-                    .min(this.delegate.matches.len().saturating_sub(1));
-            })
-            .log_err();
-        })
-    }
-
-    fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<BaseKeymapSelectorDelegate>>) {
-        if let Some(selection) = self.matches.get(self.selected_index) {
-            let base_keymap = BaseKeymap::from_names(&selection.string);
-            update_settings_file::<BaseKeymap>(self.fs.clone(), cx, move |setting| {
-                *setting = Some(base_keymap)
-            });
-        }
-
-        self.view
-            .update(cx, |_, cx| {
-                cx.emit(DismissEvent);
-            })
-            .ok();
-    }
-
-    fn dismissed(&mut self, _cx: &mut ViewContext<Picker<BaseKeymapSelectorDelegate>>) {}
-
-    fn render_match(
-        &self,
-        ix: usize,
-        selected: bool,
-        _cx: &mut gpui::ViewContext<Picker<Self>>,
-    ) -> Option<Self::ListItem> {
-        let keymap_match = &self.matches[ix];
-
-        Some(
-            ListItem::new(ix)
-                .inset(true)
-                .spacing(ListItemSpacing::Sparse)
-                .selected(selected)
-                .child(HighlightedLabel::new(
-                    keymap_match.string.clone(),
-                    keymap_match.positions.clone(),
-                )),
-        )
-    }
-}

crates/welcome2/src/base_keymap_setting.rs 🔗

@@ -1,65 +0,0 @@
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use settings::Settings;
-
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
-pub enum BaseKeymap {
-    #[default]
-    VSCode,
-    JetBrains,
-    SublimeText,
-    Atom,
-    TextMate,
-}
-
-impl BaseKeymap {
-    pub const OPTIONS: [(&'static str, Self); 5] = [
-        ("VSCode (Default)", Self::VSCode),
-        ("Atom", Self::Atom),
-        ("JetBrains", Self::JetBrains),
-        ("Sublime Text", Self::SublimeText),
-        ("TextMate", Self::TextMate),
-    ];
-
-    pub fn asset_path(&self) -> Option<&'static str> {
-        match self {
-            BaseKeymap::JetBrains => Some("keymaps/jetbrains.json"),
-            BaseKeymap::SublimeText => Some("keymaps/sublime_text.json"),
-            BaseKeymap::Atom => Some("keymaps/atom.json"),
-            BaseKeymap::TextMate => Some("keymaps/textmate.json"),
-            BaseKeymap::VSCode => None,
-        }
-    }
-
-    pub fn names() -> impl Iterator<Item = &'static str> {
-        Self::OPTIONS.iter().map(|(name, _)| *name)
-    }
-
-    pub fn from_names(option: &str) -> BaseKeymap {
-        Self::OPTIONS
-            .iter()
-            .copied()
-            .find_map(|(name, value)| (name == option).then(|| value))
-            .unwrap_or_default()
-    }
-}
-
-impl Settings for BaseKeymap {
-    const KEY: Option<&'static str> = Some("base_keymap");
-
-    type FileContent = Option<Self>;
-
-    fn load(
-        default_value: &Self::FileContent,
-        user_values: &[&Self::FileContent],
-        _: &mut gpui::AppContext,
-    ) -> anyhow::Result<Self>
-    where
-        Self: Sized,
-    {
-        Ok(user_values
-            .first()
-            .and_then(|v| **v)
-            .unwrap_or(default_value.unwrap()))
-    }
-}

crates/welcome2/src/welcome.rs 🔗

@@ -1,283 +0,0 @@
-mod base_keymap_picker;
-mod base_keymap_setting;
-
-use client::TelemetrySettings;
-use db::kvp::KEY_VALUE_STORE;
-use gpui::{
-    svg, AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView, InteractiveElement,
-    ParentElement, Render, Styled, Subscription, View, ViewContext, VisualContext, WeakView,
-    WindowContext,
-};
-use settings::{Settings, SettingsStore};
-use std::sync::Arc;
-use ui::{prelude::*, Checkbox};
-use vim::VimModeSetting;
-use workspace::{
-    dock::DockPosition,
-    item::{Item, ItemEvent},
-    open_new, AppState, Welcome, Workspace, WorkspaceId,
-};
-
-pub use base_keymap_setting::BaseKeymap;
-
-pub const FIRST_OPEN: &str = "first_open";
-
-pub fn init(cx: &mut AppContext) {
-    BaseKeymap::register(cx);
-
-    cx.observe_new_views(|workspace: &mut Workspace, _cx| {
-        workspace.register_action(|workspace, _: &Welcome, cx| {
-            let welcome_page = cx.new_view(|cx| WelcomePage::new(workspace, cx));
-            workspace.add_item(Box::new(welcome_page), cx)
-        });
-    })
-    .detach();
-
-    base_keymap_picker::init(cx);
-}
-
-pub fn show_welcome_view(app_state: &Arc<AppState>, cx: &mut AppContext) {
-    open_new(&app_state, cx, |workspace, cx| {
-        workspace.toggle_dock(DockPosition::Left, cx);
-        let welcome_page = cx.new_view(|cx| WelcomePage::new(workspace, cx));
-        workspace.add_item_to_center(Box::new(welcome_page.clone()), cx);
-        cx.focus_view(&welcome_page);
-        cx.notify();
-    })
-    .detach();
-
-    db::write_and_log(cx, || {
-        KEY_VALUE_STORE.write_kvp(FIRST_OPEN.to_string(), "false".to_string())
-    });
-}
-
-pub struct WelcomePage {
-    workspace: WeakView<Workspace>,
-    focus_handle: FocusHandle,
-    _settings_subscription: Subscription,
-}
-
-impl Render for WelcomePage {
-    fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
-        h_stack().full().track_focus(&self.focus_handle).child(
-            v_stack()
-                .w_96()
-                .gap_4()
-                .mx_auto()
-                .child(
-                    svg()
-                        .path("icons/logo_96.svg")
-                        .text_color(gpui::white())
-                        .w(px(96.))
-                        .h(px(96.))
-                        .mx_auto(),
-                )
-                .child(
-                    h_stack()
-                        .justify_center()
-                        .child(Label::new("Code at the speed of thought")),
-                )
-                .child(
-                    v_stack()
-                        .gap_2()
-                        .child(
-                            Button::new("choose-theme", "Choose a theme")
-                                .full_width()
-                                .on_click(cx.listener(|this, _, cx| {
-                                    this.workspace
-                                        .update(cx, |workspace, cx| {
-                                            theme_selector::toggle(
-                                                workspace,
-                                                &Default::default(),
-                                                cx,
-                                            )
-                                        })
-                                        .ok();
-                                })),
-                        )
-                        .child(
-                            Button::new("choose-keymap", "Choose a keymap")
-                                .full_width()
-                                .on_click(cx.listener(|this, _, cx| {
-                                    this.workspace
-                                        .update(cx, |workspace, cx| {
-                                            base_keymap_picker::toggle(
-                                                workspace,
-                                                &Default::default(),
-                                                cx,
-                                            )
-                                        })
-                                        .ok();
-                                })),
-                        )
-                        .child(
-                            Button::new("install-cli", "Install the CLI")
-                                .full_width()
-                                .on_click(cx.listener(|_, _, cx| {
-                                    cx.app_mut()
-                                        .spawn(
-                                            |cx| async move { install_cli::install_cli(&cx).await },
-                                        )
-                                        .detach_and_log_err(cx);
-                                })),
-                        ),
-                )
-                .child(
-                    v_stack()
-                        .p_3()
-                        .gap_2()
-                        .bg(cx.theme().colors().elevated_surface_background)
-                        .border_1()
-                        .border_color(cx.theme().colors().border)
-                        .rounded_md()
-                        .child(
-                            h_stack()
-                                .gap_2()
-                                .child(
-                                    Checkbox::new(
-                                        "enable-vim",
-                                        if VimModeSetting::get_global(cx).0 {
-                                            ui::Selection::Selected
-                                        } else {
-                                            ui::Selection::Unselected
-                                        },
-                                    )
-                                    .on_click(cx.listener(
-                                        move |this, selection, cx| {
-                                            this.update_settings::<VimModeSetting>(
-                                                selection,
-                                                cx,
-                                                |setting, value| *setting = Some(value),
-                                            );
-                                        },
-                                    )),
-                                )
-                                .child(Label::new("Enable vim mode")),
-                        )
-                        .child(
-                            h_stack()
-                                .gap_2()
-                                .child(
-                                    Checkbox::new(
-                                        "enable-telemetry",
-                                        if TelemetrySettings::get_global(cx).metrics {
-                                            ui::Selection::Selected
-                                        } else {
-                                            ui::Selection::Unselected
-                                        },
-                                    )
-                                    .on_click(cx.listener(
-                                        move |this, selection, cx| {
-                                            this.update_settings::<TelemetrySettings>(
-                                                selection,
-                                                cx,
-                                                |settings, value| settings.metrics = Some(value),
-                                            );
-                                        },
-                                    )),
-                                )
-                                .child(Label::new("Send anonymous usage data")),
-                        )
-                        .child(
-                            h_stack()
-                                .gap_2()
-                                .child(
-                                    Checkbox::new(
-                                        "enable-crash",
-                                        if TelemetrySettings::get_global(cx).diagnostics {
-                                            ui::Selection::Selected
-                                        } else {
-                                            ui::Selection::Unselected
-                                        },
-                                    )
-                                    .on_click(cx.listener(
-                                        move |this, selection, cx| {
-                                            this.update_settings::<TelemetrySettings>(
-                                                selection,
-                                                cx,
-                                                |settings, value| {
-                                                    settings.diagnostics = Some(value)
-                                                },
-                                            );
-                                        },
-                                    )),
-                                )
-                                .child(Label::new("Send crash reports")),
-                        ),
-                ),
-        )
-    }
-}
-
-impl WelcomePage {
-    pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
-        WelcomePage {
-            focus_handle: cx.focus_handle(),
-            workspace: workspace.weak_handle(),
-            _settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
-        }
-    }
-
-    fn update_settings<T: Settings>(
-        &mut self,
-        selection: &Selection,
-        cx: &mut ViewContext<Self>,
-        callback: impl 'static + Send + Fn(&mut T::FileContent, bool),
-    ) {
-        if let Some(workspace) = self.workspace.upgrade() {
-            let fs = workspace.read(cx).app_state().fs.clone();
-            let selection = *selection;
-            settings::update_settings_file::<T>(fs, cx, move |settings| {
-                let value = match selection {
-                    Selection::Unselected => false,
-                    Selection::Selected => true,
-                    _ => return,
-                };
-
-                callback(settings, value)
-            });
-        }
-    }
-}
-
-impl EventEmitter<ItemEvent> for WelcomePage {}
-
-impl FocusableView for WelcomePage {
-    fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
-        self.focus_handle.clone()
-    }
-}
-
-impl Item for WelcomePage {
-    type Event = ItemEvent;
-
-    fn tab_content(&self, _: Option<usize>, selected: bool, _: &WindowContext) -> AnyElement {
-        Label::new("Welcome to Zed!")
-            .color(if selected {
-                Color::Default
-            } else {
-                Color::Muted
-            })
-            .into_any_element()
-    }
-
-    fn show_toolbar(&self) -> bool {
-        false
-    }
-
-    fn clone_on_split(
-        &self,
-        _workspace_id: WorkspaceId,
-        cx: &mut ViewContext<Self>,
-    ) -> Option<View<Self>> {
-        Some(cx.new_view(|cx| WelcomePage {
-            focus_handle: cx.focus_handle(),
-            workspace: self.workspace.clone(),
-            _settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
-        }))
-    }
-
-    fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
-        f(*event)
-    }
-}

crates/zed/Cargo.toml 🔗

@@ -23,7 +23,7 @@ breadcrumbs = { package = "breadcrumbs2", path = "../breadcrumbs2" }
 call = { package = "call2", path = "../call2" }
 channel = { package = "channel2", path = "../channel2" }
 cli = { path = "../cli" }
-collab_ui = { package = "collab_ui2", path = "../collab_ui2" }
+collab_ui = { path = "../collab_ui" }
 collections = { path = "../collections" }
 command_palette = { package="command_palette2", path = "../command_palette2" }
 # component_test = { path = "../component_test" }
@@ -56,7 +56,7 @@ outline = { package = "outline2", path = "../outline2" }
 project = { package = "project2", path = "../project2" }
 project_panel = { package = "project_panel2", path = "../project_panel2" }
 project_symbols = { package = "project_symbols2", path = "../project_symbols2" }
-quick_action_bar = { package = "quick_action_bar2", path = "../quick_action_bar2" }
+quick_action_bar = { path = "../quick_action_bar" }
 recent_projects = { package = "recent_projects2", path = "../recent_projects2" }
 rope = { package = "rope2", path = "../rope2"}
 rpc = { package = "rpc2", path = "../rpc2" }
@@ -72,7 +72,7 @@ util = { path = "../util" }
 semantic_index = { package = "semantic_index2", path = "../semantic_index2" }
 vim = { package = "vim2", path = "../vim2" }
 workspace = { package = "workspace2", path = "../workspace2" }
-welcome = { package = "welcome2", path = "../welcome2" }
+welcome = { path = "../welcome" }
 zed_actions = {package = "zed_actions2", path = "../zed_actions2"}
 anyhow.workspace = true
 async-compression.workspace = true