Extract `title_bar` crate (#13597)

Nate Butler created

This PR extracts a singular title bar (`title_bar::TitleBar`) from
`ui::TitleBar` and
`collab_ui::collab_titlebar_item::CollabTitlebarItem`.

This is a first step towards organizing title bar things into one place,
and standardizing platform titlebar/window control implementations.

Release Notes:

- N/A

Change summary

Cargo.lock                                         |  44 
Cargo.toml                                         |  30 
crates/assistant/src/prompt_library.rs             |   4 
crates/collab_ui/Cargo.toml                        |   9 
crates/collab_ui/src/collab_panel.rs               |  10 
crates/collab_ui/src/collab_ui.rs                  |  71 -
crates/storybook/src/story_selector.rs             |   4 
crates/title_bar/Cargo.toml                        |  73 +
crates/title_bar/LICENSE-GPL                       |   1 
crates/title_bar/LICENSE_-GPL                      |   1 
crates/title_bar/src/call_controls.rs              |  58 +
crates/title_bar/src/collab.rs                     | 126 ++
crates/title_bar/src/platforms.rs                  |   3 
crates/title_bar/src/platforms/platform_linux.rs   |   2 
crates/title_bar/src/platforms/platform_mac.rs     |   6 
crates/title_bar/src/platforms/platform_windows.rs |   2 
crates/title_bar/src/stories/title_bar.rs          |  10 
crates/title_bar/src/title_bar.rs                  | 775 ++++++++-------
crates/ui/src/components.rs                        |   4 
crates/ui/src/components/facepile.rs               |  17 
crates/ui/src/components/stories.rs                |   2 
crates/ui/src/components/title_bar/title_bar.rs    | 135 --
22 files changed, 744 insertions(+), 643 deletions(-)

Detailed changes

Cargo.lock πŸ”—

@@ -2551,18 +2551,13 @@ name = "collab_ui"
 version = "0.1.0"
 dependencies = [
  "anyhow",
- "auto_update",
  "call",
  "channel",
  "client",
  "collections",
- "command_palette",
  "db",
- "dev_server_projects",
  "editor",
  "emojis",
- "extensions_ui",
- "feedback",
  "futures 0.3.28",
  "fuzzy",
  "gpui",
@@ -2575,7 +2570,6 @@ dependencies = [
  "picker",
  "pretty_assertions",
  "project",
- "recent_projects",
  "release_channel",
  "rich_text",
  "rpc",
@@ -2587,15 +2581,14 @@ dependencies = [
  "smallvec",
  "story",
  "theme",
- "theme_selector",
  "time",
  "time_format",
+ "title_bar",
  "tree-sitter-markdown",
  "ui",
  "util",
  "vcs_menu",
  "workspace",
- "zed_actions",
 ]
 
 [[package]]
@@ -11046,6 +11039,41 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
 
+[[package]]
+name = "title_bar"
+version = "0.1.0"
+dependencies = [
+ "auto_update",
+ "call",
+ "client",
+ "collections",
+ "command_palette",
+ "dev_server_projects",
+ "editor",
+ "extensions_ui",
+ "feedback",
+ "gpui",
+ "http 0.1.0",
+ "notifications",
+ "pretty_assertions",
+ "project",
+ "recent_projects",
+ "rpc",
+ "serde",
+ "settings",
+ "smallvec",
+ "story",
+ "theme",
+ "theme_selector",
+ "tree-sitter-markdown",
+ "ui",
+ "util",
+ "vcs_menu",
+ "windows 0.57.0",
+ "workspace",
+ "zed_actions",
+]
+
 [[package]]
 name = "tokio"
 version = "1.37.0"

Cargo.toml πŸ”—

@@ -21,6 +21,7 @@ members = [
     "crates/command_palette_hooks",
     "crates/copilot",
     "crates/db",
+    "crates/dev_server_projects",
     "crates/diagnostics",
     "crates/editor",
     "crates/extension",
@@ -77,14 +78,11 @@ members = [
     "crates/refineable",
     "crates/refineable/derive_refineable",
     "crates/release_channel",
-    "crates/dev_server_projects",
     "crates/repl",
     "crates/rich_text",
     "crates/rope",
     "crates/rpc",
     "crates/rustdoc",
-    "crates/task",
-    "crates/tasks_ui",
     "crates/search",
     "crates/semantic_index",
     "crates/semantic_version",
@@ -95,17 +93,20 @@ members = [
     "crates/story",
     "crates/storybook",
     "crates/sum_tree",
-    "crates/tab_switcher",
     "crates/supermaven",
     "crates/supermaven_api",
+    "crates/tab_switcher",
+    "crates/task",
+    "crates/tasks_ui",
+    "crates/telemetry_events",
     "crates/terminal",
     "crates/terminal_view",
     "crates/text",
     "crates/theme",
     "crates/theme_importer",
     "crates/theme_selector",
-    "crates/telemetry_events",
     "crates/time_format",
+    "crates/title_bar",
     "crates/ui",
     "crates/ui_text_field",
     "crates/util",
@@ -175,6 +176,7 @@ command_palette_hooks = { path = "crates/command_palette_hooks" }
 copilot = { path = "crates/copilot" }
 dashmap = "5.5.3"
 db = { path = "crates/db" }
+dev_server_projects = { path = "crates/dev_server_projects" }
 diagnostics = { path = "crates/diagnostics" }
 editor = { path = "crates/editor" }
 extension = { path = "crates/extension" }
@@ -195,9 +197,9 @@ gpui_macros = { path = "crates/gpui_macros" }
 headless = { path = "crates/headless" }
 html_to_markdown = { path = "crates/html_to_markdown" }
 http = { path = "crates/http" }
-install_cli = { path = "crates/install_cli" }
 image_viewer = { path = "crates/image_viewer" }
 inline_completion_button = { path = "crates/inline_completion_button" }
+install_cli = { path = "crates/install_cli" }
 journal = { path = "crates/journal" }
 language = { path = "crates/language" }
 language_selector = { path = "crates/language_selector" }
@@ -223,21 +225,17 @@ plugin = { path = "crates/plugin" }
 plugin_macros = { path = "crates/plugin_macros" }
 prettier = { path = "crates/prettier" }
 project = { path = "crates/project" }
-proto = { path = "crates/proto" }
-worktree = { path = "crates/worktree" }
 project_panel = { path = "crates/project_panel" }
 project_symbols = { path = "crates/project_symbols" }
+proto = { path = "crates/proto" }
 quick_action_bar = { path = "crates/quick_action_bar" }
 recent_projects = { path = "crates/recent_projects" }
 release_channel = { path = "crates/release_channel" }
-dev_server_projects = { path = "crates/dev_server_projects" }
 repl = { path = "crates/repl" }
 rich_text = { path = "crates/rich_text" }
 rope = { path = "crates/rope" }
 rpc = { path = "crates/rpc" }
 rustdoc = { path = "crates/rustdoc" }
-task = { path = "crates/task" }
-tasks_ui = { path = "crates/tasks_ui" }
 search = { path = "crates/search" }
 semantic_index = { path = "crates/semantic_index" }
 semantic_version = { path = "crates/semantic_version" }
@@ -245,20 +243,23 @@ settings = { path = "crates/settings" }
 snippet = { path = "crates/snippet" }
 sqlez = { path = "crates/sqlez" }
 sqlez_macros = { path = "crates/sqlez_macros" }
-supermaven = { path = "crates/supermaven" }
-supermaven_api = { path = "crates/supermaven_api" }
 story = { path = "crates/story" }
 storybook = { path = "crates/storybook" }
 sum_tree = { path = "crates/sum_tree" }
+supermaven = { path = "crates/supermaven" }
+supermaven_api = { path = "crates/supermaven_api" }
 tab_switcher = { path = "crates/tab_switcher" }
+task = { path = "crates/task" }
+tasks_ui = { path = "crates/tasks_ui" }
+telemetry_events = { path = "crates/telemetry_events" }
 terminal = { path = "crates/terminal" }
 terminal_view = { path = "crates/terminal_view" }
 text = { path = "crates/text" }
 theme = { path = "crates/theme" }
 theme_importer = { path = "crates/theme_importer" }
 theme_selector = { path = "crates/theme_selector" }
-telemetry_events = { path = "crates/telemetry_events" }
 time_format = { path = "crates/time_format" }
+title_bar = { path = "crates/title_bar" }
 ui = { path = "crates/ui" }
 ui_text_field = { path = "crates/ui_text_field" }
 util = { path = "crates/util" }
@@ -266,6 +267,7 @@ vcs_menu = { path = "crates/vcs_menu" }
 vim = { path = "crates/vim" }
 welcome = { path = "crates/welcome" }
 workspace = { path = "crates/workspace" }
+worktree = { path = "crates/worktree" }
 zed = { path = "crates/zed" }
 zed_actions = { path = "crates/zed_actions" }
 

crates/assistant/src/prompt_library.rs πŸ”—

@@ -34,7 +34,7 @@ use std::{
 use theme::ThemeSettings;
 use ui::{
     div, prelude::*, IconButtonShape, ListItem, ListItemSpacing, ParentElement, Render,
-    SharedString, Styled, TitleBar, Tooltip, ViewContext, VisualContext,
+    SharedString, Styled, Tooltip, ViewContext, VisualContext,
 };
 use util::{ResultExt, TryFutureExt};
 use uuid::Uuid;
@@ -751,7 +751,7 @@ impl PromptLibrary {
             .child(
                 h_flex()
                     .p(Spacing::Small.rems(cx))
-                    .h(TitleBar::height(cx))
+                    .h_9()
                     .w_full()
                     .flex_none()
                     .justify_end()

crates/collab_ui/Cargo.toml πŸ”—

@@ -30,17 +30,13 @@ test-support = [
 
 [dependencies]
 anyhow.workspace = true
-auto_update.workspace = true
 call.workspace = true
 channel.workspace = true
 client.workspace = true
 collections.workspace = true
-command_palette.workspace = true
 db.workspace = true
 editor.workspace = true
 emojis.workspace = true
-extensions_ui.workspace = true
-feedback.workspace = true
 futures.workspace = true
 fuzzy.workspace = true
 gpui.workspace = true
@@ -51,8 +47,6 @@ notifications.workspace = true
 parking_lot.workspace = true
 picker.workspace = true
 project.workspace = true
-recent_projects.workspace = true
-dev_server_projects.workspace = true
 release_channel.workspace = true
 rich_text.workspace = true
 rpc.workspace = true
@@ -64,14 +58,13 @@ settings.workspace = true
 smallvec.workspace = true
 story = { workspace = true, optional = true }
 theme.workspace = true
-theme_selector.workspace = true
 time_format.workspace = true
 time.workspace = true
+title_bar.workspace = true
 ui.workspace = true
 util.workspace = true
 vcs_menu.workspace = true
 workspace.workspace = true
-zed_actions.workspace = true
 
 [dev-dependencies]
 call = { workspace = true, features = ["test-support"] }

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

@@ -2,10 +2,7 @@ mod channel_modal;
 mod contact_finder;
 
 use self::channel_modal::ChannelModal;
-use crate::{
-    channel_view::ChannelView, chat_panel::ChatPanel, face_pile::FacePile,
-    CollaborationPanelSettings,
-};
+use crate::{channel_view::ChannelView, chat_panel::ChatPanel, CollaborationPanelSettings};
 use call::ActiveCall;
 use channel::{Channel, ChannelEvent, ChannelStore};
 use client::{ChannelId, Client, Contact, ProjectId, User, UserStore};
@@ -34,7 +31,8 @@ use std::{mem, sync::Arc};
 use theme::{ActiveTheme, ThemeSettings};
 use ui::{
     prelude::*, tooltip_container, Avatar, AvatarAvailabilityIndicator, Button, Color, ContextMenu,
-    Icon, IconButton, IconName, IconSize, Indicator, Label, ListHeader, ListItem, Tooltip,
+    Facepile, Icon, IconButton, IconName, IconSize, Indicator, Label, ListHeader, ListItem,
+    Tooltip,
 };
 use util::{maybe, ResultExt, TryFutureExt};
 use workspace::{
@@ -2542,7 +2540,7 @@ impl CollabPanel {
             None
         } else {
             let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
-            let result = FacePile::new(
+            let result = Facepile::new(
                 participants
                     .iter()
                     .map(|user| Avatar::new(user.avatar_uri.clone()).into_any_element())

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

@@ -1,20 +1,16 @@
 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};
 pub use collab_panel::CollabPanel;
-pub use collab_titlebar_item::CollabTitlebarItem;
 use gpui::{
-    actions, point, AppContext, Pixels, PlatformDisplay, Size, Task, WindowBackgroundAppearance,
-    WindowBounds, WindowContext, WindowKind, WindowOptions,
+    point, AppContext, Pixels, PlatformDisplay, Size, WindowBackgroundAppearance, WindowBounds,
+    WindowKind, WindowOptions,
 };
 use panel_settings::MessageEditorSettings;
 pub use panel_settings::{
@@ -23,12 +19,7 @@ pub use panel_settings::{
 use release_channel::ReleaseChannel;
 use settings::Settings;
 use ui::px;
-use workspace::{notifications::DetachAndPromptErr, AppState};
-
-actions!(
-    collab,
-    [ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall]
-);
+use workspace::AppState;
 
 pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
     CollaborationPanelSettings::register(cx);
@@ -36,63 +27,13 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
     NotificationPanelSettings::register(cx);
     MessageEditorSettings::register(cx);
 
-    vcs_menu::init(cx);
-    collab_titlebar_item::init(cx);
-    collab_panel::init(cx);
     channel_view::init(cx);
     chat_panel::init(cx);
+    collab_panel::init(cx);
     notification_panel::init(cx);
     notifications::init(&app_state, cx);
-}
-
-pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut WindowContext) {
-    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,
-                );
-                Task::ready(room.unshare_screen(cx))
-            } else {
-                report_call_event_for_room(
-                    "enable screen share",
-                    room.id(),
-                    room.channel_id(),
-                    &client,
-                );
-                room.share_screen(cx)
-            }
-        });
-        toggle_screen_sharing.detach_and_prompt_err("Sharing Screen Failed", cx, |e, _| Some(format!("{:?}\n\nPlease check that you have given Zed permissions to record your screen in Settings.", e)));
-    }
-}
-
-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() {
-                "enable microphone"
-            } else {
-                "disable microphone"
-            };
-            report_call_event_for_room(operation, room.id(), room.channel_id(), &client);
-
-            room.toggle_mute(cx)
-        });
-    }
-}
-
-pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) {
-    if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
-        room.update(cx, |room, cx| room.toggle_deafen(cx));
-    }
+    title_bar::init(cx);
+    vcs_menu::init(cx);
 }
 
 fn notification_window_options(

crates/storybook/src/story_selector.rs πŸ”—

@@ -35,7 +35,7 @@ pub enum ComponentStory {
     Tab,
     TabBar,
     Text,
-    TitleBar,
+    // TitleBar,
     ToggleButton,
     ToolStrip,
     ViewportUnits,
@@ -69,7 +69,7 @@ impl ComponentStory {
             Self::Text => TextStory::view(cx).into(),
             Self::Tab => cx.new_view(|_| ui::TabStory).into(),
             Self::TabBar => cx.new_view(|_| ui::TabBarStory).into(),
-            Self::TitleBar => cx.new_view(|_| ui::TitleBarStory).into(),
+            // Self::TitleBar => cx.new_view(|_| title_bar::TitleBarStory).into(),
             Self::ToggleButton => cx.new_view(|_| ui::ToggleButtonStory).into(),
             Self::ToolStrip => cx.new_view(|_| ui::ToolStripStory).into(),
             Self::ViewportUnits => cx.new_view(|_| crate::stories::ViewportUnitsStory).into(),

crates/title_bar/Cargo.toml πŸ”—

@@ -0,0 +1,73 @@
+[package]
+name = "title_bar"
+version = "0.1.0"
+edition = "2021"
+publish = false
+license = "GPL-3.0-or-later"
+
+[lints]
+workspace = true
+
+[lib]
+path = "src/title_bar.rs"
+doctest = false
+
+[features]
+default = []
+stories = ["dep:story"]
+test-support = [
+    "call/test-support",
+    "client/test-support",
+    "collections/test-support",
+    "editor/test-support",
+    "gpui/test-support",
+    "http/test-support",
+    "project/test-support",
+    "settings/test-support",
+    "util/test-support",
+    "workspace/test-support",
+]
+
+[dependencies]
+auto_update.workspace = true
+call.workspace = true
+client.workspace = true
+command_palette.workspace = true
+dev_server_projects.workspace = true
+extensions_ui.workspace = true
+feedback.workspace = true
+gpui.workspace = true
+notifications.workspace = true
+project.workspace = true
+recent_projects.workspace = true
+rpc.workspace = true
+serde.workspace = true
+settings.workspace = true
+smallvec.workspace = true
+story = { workspace = true, optional = true }
+theme.workspace = true
+theme_selector.workspace = true
+ui.workspace = true
+util.workspace = true
+vcs_menu.workspace = true
+workspace.workspace = true
+zed_actions.workspace = true
+
+[target.'cfg(windows)'.dependencies]
+windows.workspace = true
+
+[dev-dependencies]
+call = { workspace = true, features = ["test-support"] }
+client = { workspace = true, features = ["test-support"] }
+collections = { workspace = true, features = ["test-support"] }
+editor = { workspace = true, features = ["test-support"] }
+gpui = { workspace = true, features = ["test-support"] }
+http = { workspace = true, features = ["test-support"] }
+notifications = { workspace = true, features = ["test-support"] }
+pretty_assertions.workspace = true
+project = { workspace = true, features = ["test-support"] }
+rpc = { workspace = true, features = ["test-support"] }
+settings = { workspace = true, features = ["test-support"] }
+tree-sitter-markdown.workspace = true
+util = { workspace = true, features = ["test-support"] }
+workspace = { workspace = true, features = ["test-support"] }

crates/title_bar/src/call_controls.rs πŸ”—

@@ -0,0 +1,58 @@
+use call::{report_call_event_for_room, ActiveCall};
+use gpui::{actions, AppContext, Task, WindowContext};
+use workspace::notifications::DetachAndPromptErr;
+
+actions!(
+    collab,
+    [ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall]
+);
+
+pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut WindowContext) {
+    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,
+                );
+                Task::ready(room.unshare_screen(cx))
+            } else {
+                report_call_event_for_room(
+                    "enable screen share",
+                    room.id(),
+                    room.channel_id(),
+                    &client,
+                );
+                room.share_screen(cx)
+            }
+        });
+        toggle_screen_sharing.detach_and_prompt_err("Sharing Screen Failed", cx, |e, _| Some(format!("{:?}\n\nPlease check that you have given Zed permissions to record your screen in Settings.", e)));
+    }
+}
+
+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() {
+                "enable microphone"
+            } else {
+                "disable microphone"
+            };
+            report_call_event_for_room(operation, room.id(), room.channel_id(), &client);
+
+            room.toggle_mute(cx)
+        });
+    }
+}
+
+pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) {
+    if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
+        room.update(cx, |room, cx| room.toggle_deafen(cx));
+    }
+}

crates/title_bar/src/collab.rs πŸ”—

@@ -0,0 +1,126 @@
+use crate::TitleBar;
+use call::Room;
+use client::{proto::PeerId, User};
+use gpui::{canvas, point, Hsla, IntoElement, Path, Styled};
+use rpc::proto::{self};
+use std::sync::Arc;
+use theme::ActiveTheme;
+use ui::{prelude::*, Avatar, AvatarAudioStatusIndicator, Facepile, Tooltip};
+
+pub(crate) fn render_color_ribbon(color: Hsla) -> impl Element {
+    canvas(
+        move |_, _| {},
+        move |bounds, _, cx| {
+            let height = bounds.size.height;
+            let horizontal_offset = height;
+            let vertical_offset = px(height.0 / 2.0);
+            let mut path = Path::new(bounds.lower_left());
+            path.curve_to(
+                bounds.origin + point(horizontal_offset, vertical_offset),
+                bounds.origin + point(px(0.0), vertical_offset),
+            );
+            path.line_to(bounds.upper_right() + point(-horizontal_offset, vertical_offset));
+            path.curve_to(
+                bounds.lower_right(),
+                bounds.upper_right() + point(px(0.0), vertical_offset),
+            );
+            path.line_to(bounds.lower_left());
+            cx.paint_path(path, color);
+        },
+    )
+    .h_1()
+    .w_full()
+}
+
+impl TitleBar {
+    #[allow(clippy::too_many_arguments)]
+    pub(crate) fn render_collaborator(
+        &self,
+        user: &Arc<User>,
+        peer_id: PeerId,
+        is_present: bool,
+        is_speaking: bool,
+        is_muted: bool,
+        leader_selection_color: Option<Hsla>,
+        room: &Room,
+        project_id: Option<u64>,
+        current_user: &Arc<User>,
+        cx: &ViewContext<Self>,
+    ) -> Option<Div> {
+        if room.role_for_user(user.id) == Some(proto::ChannelRole::Guest) {
+            return None;
+        }
+
+        const FACEPILE_LIMIT: usize = 3;
+        let followers = project_id.map_or(&[] as &[_], |id| room.followers_for(peer_id, id));
+        let extra_count = followers.len().saturating_sub(FACEPILE_LIMIT);
+
+        Some(
+            div()
+                .m_0p5()
+                .p_0p5()
+                // When the collaborator is not followed, still draw this wrapper div, but leave
+                // it transparent, so that it does not shift the layout when following.
+                .when_some(leader_selection_color, |div, color| {
+                    div.rounded_md().bg(color)
+                })
+                .child(
+                    Facepile::empty()
+                        .child(
+                            Avatar::new(user.avatar_uri.clone())
+                                .grayscale(!is_present)
+                                .border_color(if is_speaking {
+                                    cx.theme().status().info
+                                } else {
+                                    // We draw the border in a transparent color rather to avoid
+                                    // the layout shift that would come with adding/removing the border.
+                                    gpui::transparent_black()
+                                })
+                                .when(is_muted, |avatar| {
+                                    avatar.indicator(
+                                        AvatarAudioStatusIndicator::new(ui::AudioStatus::Muted)
+                                            .tooltip({
+                                                let github_login = user.github_login.clone();
+                                                move |cx| {
+                                                    Tooltip::text(
+                                                        format!("{} is muted", github_login),
+                                                        cx,
+                                                    )
+                                                }
+                                            }),
+                                    )
+                                }),
+                        )
+                        .children(followers.iter().take(FACEPILE_LIMIT).filter_map(
+                            |follower_peer_id| {
+                                let follower = room
+                                    .remote_participants()
+                                    .values()
+                                    .find_map(|p| {
+                                        (p.peer_id == *follower_peer_id).then_some(&p.user)
+                                    })
+                                    .or_else(|| {
+                                        (self.client.peer_id() == Some(*follower_peer_id))
+                                            .then_some(current_user)
+                                    })?
+                                    .clone();
+
+                                Some(div().mt(-px(4.)).child(
+                                    Avatar::new(follower.avatar_uri.clone()).size(rems(0.75)),
+                                ))
+                            },
+                        ))
+                        .children(if extra_count > 0 {
+                            Some(
+                                div()
+                                    .ml_1()
+                                    .child(Label::new(format!("+{extra_count}")))
+                                    .into_any_element(),
+                            )
+                        } else {
+                            None
+                        }),
+                ),
+        )
+    }
+}

crates/ui/src/components/title_bar/linux_window_controls.rs β†’ crates/title_bar/src/platforms/platform_linux.rs πŸ”—

@@ -1,6 +1,6 @@
 use gpui::{prelude::*, Action, Rgba, WindowAppearance};
 
-use crate::prelude::*;
+use ui::prelude::*;
 
 #[derive(IntoElement)]
 pub struct LinuxWindowControls {

crates/title_bar/src/platforms/platform_mac.rs πŸ”—

@@ -0,0 +1,6 @@
+/// 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.
+///
+/// Magic number: There is one extra pixel of padding on the left side due to
+/// the 1px border around the window on macOS apps.
+pub const TRAFFIC_LIGHT_PADDING: f32 = 71.;

crates/ui/src/components/title_bar/windows_window_controls.rs β†’ crates/title_bar/src/platforms/platform_windows.rs πŸ”—

@@ -1,6 +1,6 @@
 use gpui::{prelude::*, Rgba, WindowAppearance};
 
-use crate::prelude::*;
+use ui::prelude::*;
 
 #[derive(IntoElement)]
 pub struct WindowsWindowControls {

crates/ui/src/components/stories/title_bar.rs β†’ crates/title_bar/src/stories/title_bar.rs πŸ”—

@@ -1,13 +1,13 @@
 use gpui::{NoAction, Render};
 use story::{StoryContainer, StoryItem, StorySection};
 
-use crate::{prelude::*, PlatformStyle, TitleBar};
+use crate::{prelude::*, PlatformStyle, UiTitleBar};
 
 pub struct TitleBarStory;
 
 impl Render for TitleBarStory {
     fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
-        fn add_sample_children(titlebar: TitleBar) -> TitleBar {
+        fn add_sample_children(titlebar: UiTitleBar) -> UiTitleBar {
             titlebar
                 .child(div().size_2().bg(gpui::red()))
                 .child(div().size_2().bg(gpui::blue()))
@@ -19,7 +19,7 @@ impl Render for TitleBarStory {
                 StorySection::new().child(
                     StoryItem::new(
                         "Default (macOS)",
-                        TitleBar::new("macos", Box::new(NoAction))
+                        UiTitleBar::new("macos", Box::new(NoAction))
                             .platform_style(PlatformStyle::Mac)
                             .map(add_sample_children),
                     )
@@ -31,7 +31,7 @@ impl Render for TitleBarStory {
                 StorySection::new().child(
                     StoryItem::new(
                         "Default (Linux)",
-                        TitleBar::new("linux", Box::new(NoAction))
+                        UiTitleBar::new("linux", Box::new(NoAction))
                             .platform_style(PlatformStyle::Linux)
                             .map(add_sample_children),
                     )
@@ -43,7 +43,7 @@ impl Render for TitleBarStory {
                 StorySection::new().child(
                     StoryItem::new(
                         "Default (Windows)",
-                        TitleBar::new("windows", Box::new(NoAction))
+                        UiTitleBar::new("windows", Box::new(NoAction))
                             .platform_style(PlatformStyle::Windows)
                             .map(add_sample_children),
                     )

crates/collab_ui/src/collab_titlebar_item.rs β†’ crates/title_bar/src/title_bar.rs πŸ”—

@@ -1,21 +1,27 @@
-use crate::face_pile::FacePile;
+mod call_controls;
+mod collab;
+mod platforms;
+
+use crate::platforms::{platform_linux, platform_mac, platform_windows};
 use auto_update::AutoUpdateStatus;
-use call::{ActiveCall, ParticipantLocation, Room};
-use client::{proto::PeerId, Client, User, UserStore};
+use call::{ActiveCall, ParticipantLocation};
+use client::{Client, UserStore};
+use collab::render_color_ribbon;
 use gpui::{
-    actions, canvas, div, point, px, Action, AnyElement, AppContext, Element, Hsla,
-    InteractiveElement, IntoElement, Model, ParentElement, Path, Render,
-    StatefulInteractiveElement, Styled, Subscription, ViewContext, VisualContext, WeakView,
+    actions, div, px, Action, AnyElement, AppContext, Element, InteractiveElement, Interactivity,
+    IntoElement, Model, ParentElement, Render, Stateful, StatefulInteractiveElement, Styled,
+    Subscription, ViewContext, VisualContext, WeakView,
 };
 use project::{Project, RepositoryEntry};
 use recent_projects::RecentProjects;
-use rpc::proto::{self, DevServerStatus};
+use rpc::proto::DevServerStatus;
 use settings::Settings;
+use smallvec::SmallVec;
 use std::sync::Arc;
 use theme::{ActiveTheme, ThemeSettings};
 use ui::{
-    h_flex, prelude::*, Avatar, AvatarAudioStatusIndicator, Button, ButtonLike, ButtonStyle,
-    ContextMenu, Icon, IconButton, IconName, Indicator, PopoverMenu, TintColor, TitleBar, Tooltip,
+    h_flex, prelude::*, Avatar, Button, ButtonLike, ButtonStyle, ContextMenu, Icon, IconButton,
+    IconName, Indicator, PopoverMenu, TintColor, Tooltip,
 };
 use util::ResultExt;
 use vcs_menu::{BranchList, OpenRecent as ToggleVcsMenu};
@@ -37,13 +43,16 @@ actions!(
 
 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)
+        let item = cx.new_view(|cx| TitleBar::new("title-bar", workspace, cx));
+        workspace.set_titlebar_item(item.into(), cx)
     })
     .detach();
 }
 
-pub struct CollabTitlebarItem {
+pub struct TitleBar {
+    platform_style: PlatformStyle,
+    content: Stateful<Div>,
+    children: SmallVec<[AnyElement; 2]>,
     project: Model<Project>,
     user_store: Model<UserStore>,
     client: Arc<Client>,
@@ -51,319 +60,348 @@ pub struct CollabTitlebarItem {
     _subscriptions: Vec<Subscription>,
 }
 
-impl Render for CollabTitlebarItem {
+impl Render for TitleBar {
     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();
         let workspace = self.workspace.upgrade();
+        let close_action = Box::new(workspace::CloseWindow);
 
         let platform_supported = cfg!(target_os = "macos");
-
-        TitleBar::new("collab-titlebar", Box::new(workspace::CloseWindow))
-            // note: on windows titlebar behaviour is handled by the platform implementation
-            .when(cfg!(not(windows)), |this| {
-                this.on_click(|event, cx| {
-                    if event.up.click_count == 2 {
-                        cx.zoom_window();
-                    }
-                })
+        let height = Self::height(cx);
+
+        h_flex()
+            .id("titlebar")
+            .w_full()
+            .pt(Self::top_padding(cx))
+            .h(height + Self::top_padding(cx))
+            .map(|this| {
+                if cx.is_fullscreen() {
+                    this.pl_2()
+                } else if self.platform_style == PlatformStyle::Mac {
+                    this.pl(px(platform_mac::TRAFFIC_LIGHT_PADDING))
+                } else {
+                    this.pl_2()
+                }
             })
-            // left side
+            .bg(cx.theme().colors().title_bar_background)
+            .content_stretch()
             .child(
-                h_flex()
-                    .gap_1()
-                    .children(self.render_application_menu(cx))
-                    .children(self.render_project_host(cx))
-                    .child(self.render_project_name(cx))
-                    .children(self.render_project_branch(cx))
-                    .on_mouse_move(|_, cx| cx.stop_propagation()),
-            )
-            .child(
-                h_flex()
-                    .id("collaborator-list")
+                div()
+                    .id("titlebar-content")
+                    .flex()
+                    .flex_row()
+                    .justify_between()
                     .w_full()
-                    .gap_1()
-                    .overflow_x_scroll()
-                    .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);
-
-                            let current_user_face_pile = self.render_collaborator(
-                                &current_user,
-                                peer_id,
-                                true,
-                                room.is_speaking(),
-                                room.is_muted(),
-                                None,
-                                &room,
-                                project_id,
-                                &current_user,
-                                cx,
-                            );
-
-                            this.children(current_user_face_pile.map(|face_pile| {
-                                v_flex()
-                                    .on_mouse_move(|_, cx| cx.stop_propagation())
-                                    .child(face_pile)
-                                    .child(render_color_ribbon(player_colors.local().cursor))
-                            }))
-                            .children(
-                                remote_participants.iter().filter_map(|collaborator| {
-                                    let player_color = player_colors
-                                        .color_for_participant(collaborator.participant_index.0);
-                                    let is_following = workspace
-                                        .as_ref()?
-                                        .read(cx)
-                                        .is_being_followed(collaborator.peer_id);
-                                    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,
-                                        is_following.then_some(player_color.selection),
-                                        &room,
-                                        project_id,
-                                        &current_user,
-                                        cx,
-                                    )?;
-
-                                    Some(
-                                        v_flex()
-                                            .id(("collaborator", collaborator.user.id))
-                                            .child(face_pile)
-                                            .child(render_color_ribbon(player_color.cursor))
-                                            .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);
+                    // note: on windows titlebar behaviour is handled by the platform implementation
+                    .when(cfg!(not(windows)), |this| {
+                        this.on_click(|event, cx| {
+                            if event.up.click_count == 2 {
+                                cx.zoom_window();
+                            }
+                        })
+                    })
+                        // left side
+                        .child(
+                            h_flex()
+                                .gap_1()
+                                .children(self.render_application_menu(cx))
+                                .children(self.render_project_host(cx))
+                                .child(self.render_project_name(cx))
+                                .children(self.render_project_branch(cx))
+                                .on_mouse_move(|_, cx| cx.stop_propagation()),
+                        )
+                        .child(
+                            h_flex()
+                                .id("collaborator-list")
+                                .w_full()
+                                .gap_1()
+                                .overflow_x_scroll()
+                                .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);
+
+                                        let current_user_face_pile = self.render_collaborator(
+                                            &current_user,
+                                            peer_id,
+                                            true,
+                                            room.is_speaking(),
+                                            room.is_muted(),
+                                            None,
+                                            &room,
+                                            project_id,
+                                            &current_user,
+                                            cx,
+                                        );
+
+                                        this.children(current_user_face_pile.map(|face_pile| {
+                                            v_flex()
+                                                .on_mouse_move(|_, cx| cx.stop_propagation())
+                                                .child(face_pile)
+                                                .child(render_color_ribbon(player_colors.local().cursor))
+                                        }))
+                                        .children(
+                                            remote_participants.iter().filter_map(|collaborator| {
+                                                let player_color = player_colors
+                                                    .color_for_participant(collaborator.participant_index.0);
+                                                let is_following = workspace
+                                                    .as_ref()?
+                                                    .read(cx)
+                                                    .is_being_followed(collaborator.peer_id);
+                                                let is_present = project_id.map_or(false, |project_id| {
+                                                    collaborator.location
+                                                        == ParticipantLocation::SharedProject { project_id }
+                                                });
+
+                                                let facepile = self.render_collaborator(
+                                                    &collaborator.user,
+                                                    collaborator.peer_id,
+                                                    is_present,
+                                                    collaborator.speaking,
+                                                    collaborator.muted,
+                                                    is_following.then_some(player_color.selection),
+                                                    &room,
+                                                    project_id,
+                                                    &current_user,
+                                                    cx,
+                                                )?;
+
+                                                Some(
+                                                    v_flex()
+                                                        .id(("collaborator", collaborator.user.id))
+                                                        .child(facepile)
+                                                        .child(render_color_ribbon(player_color.cursor))
+                                                        .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();
+                                                            })
                                                         })
-                                                        .ok();
-                                                })
-                                            })
-                                            .tooltip({
-                                                let login = collaborator.user.github_login.clone();
-                                                move |cx| {
-                                                    Tooltip::text(format!("Follow {login}"), cx)
-                                                }
+                                                        .tooltip({
+                                                            let login = collaborator.user.github_login.clone();
+                                                            move |cx| {
+                                                                Tooltip::text(format!("Follow {login}"), cx)
+                                                            }
+                                                        }),
+                                                )
                                             }),
+                                        )
+                                    },
+                                ),
+                        )
+                        // right side
+                        .child(
+                            h_flex()
+                                .gap_1()
+                                .pr_1()
+                                .on_mouse_move(|_, cx| cx.stop_propagation())
+                                .when_some(room, |this, room| {
+                                    let room = room.read(cx);
+                                    let project = self.project.read(cx);
+                                    let is_local = project.is_local();
+                                    let is_dev_server_project = project.dev_server_project_id().is_some();
+                                    let is_shared = (is_local || is_dev_server_project) && project.is_shared();
+                                    let is_muted = room.is_muted();
+                                    let is_deafened = room.is_deafened().unwrap_or(false);
+                                    let is_screen_sharing = room.is_screen_sharing();
+                                    let can_use_microphone = room.can_use_microphone();
+                                    let can_share_projects = room.can_share_projects();
+
+                                    this.when(
+                                        (is_local || is_dev_server_project) && can_share_projects,
+                                        |this| {
+                                            this.child(
+                                                Button::new(
+                                                    "toggle_sharing",
+                                                    if is_shared { "Unshare" } else { "Share" },
+                                                )
+                                                .tooltip(move |cx| {
+                                                    Tooltip::text(
+                                                        if is_shared {
+                                                            "Stop sharing project with call participants"
+                                                        } else {
+                                                            "Share project with call participants"
+                                                        },
+                                                        cx,
+                                                    )
+                                                })
+                                                .style(ButtonStyle::Subtle)
+                                                .selected_style(ButtonStyle::Tinted(TintColor::Accent))
+                                                .selected(is_shared)
+                                                .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);
+                                                        }
+                                                    },
+                                                )),
+                                            )
+                                        },
                                     )
-                                }),
-                            )
-                        },
-                    ),
-            )
-            // right side
-            .child(
-                h_flex()
-                    .gap_1()
-                    .pr_1()
-                    .on_mouse_move(|_, cx| cx.stop_propagation())
-                    .when_some(room, |this, room| {
-                        let room = room.read(cx);
-                        let project = self.project.read(cx);
-                        let is_local = project.is_local();
-                        let is_dev_server_project = project.dev_server_project_id().is_some();
-                        let is_shared = (is_local || is_dev_server_project) && project.is_shared();
-                        let is_muted = room.is_muted();
-                        let is_deafened = room.is_deafened().unwrap_or(false);
-                        let is_screen_sharing = room.is_screen_sharing();
-                        let can_use_microphone = room.can_use_microphone();
-                        let can_share_projects = room.can_share_projects();
-
-                        this.when(
-                            (is_local || is_dev_server_project) && can_share_projects,
-                            |this| {
-                                this.child(
-                                    Button::new(
-                                        "toggle_sharing",
-                                        if is_shared { "Unshare" } else { "Share" },
+                                    .child(
+                                        div()
+                                            .child(
+                                                IconButton::new("leave-call", ui::IconName::Exit)
+                                                    .style(ButtonStyle::Subtle)
+                                                    .tooltip(|cx| Tooltip::text("Leave call", cx))
+                                                    .icon_size(IconSize::Small)
+                                                    .on_click(move |_, cx| {
+                                                        ActiveCall::global(cx)
+                                                            .update(cx, |call, cx| call.hang_up(cx))
+                                                            .detach_and_log_err(cx);
+                                                    }),
+                                            )
+                                            .pr_2(),
                                     )
-                                    .tooltip(move |cx| {
-                                        Tooltip::text(
-                                            if is_shared {
-                                                "Stop sharing project with call participants"
-                                            } else {
-                                                "Share project with call participants"
-                                            },
-                                            cx,
+                                    .when(can_use_microphone, |this| {
+                                        this.child(
+                                            IconButton::new(
+                                                "mute-microphone",
+                                                if is_muted {
+                                                    ui::IconName::MicMute
+                                                } else {
+                                                    ui::IconName::Mic
+                                                },
+                                            )
+                                            .tooltip(move |cx| {
+                                                Tooltip::text(
+                                                    if !platform_supported {
+                                                        "Cannot share microphone"
+                                                    } else if is_muted {
+                                                        "Unmute microphone"
+                                                    } else {
+                                                        "Mute microphone"
+                                                    },
+                                                    cx,
+                                                )
+                                            })
+                                            .style(ButtonStyle::Subtle)
+                                            .icon_size(IconSize::Small)
+                                            .selected(platform_supported && is_muted)
+                                            .disabled(!platform_supported)
+                                            .selected_style(ButtonStyle::Tinted(TintColor::Negative))
+                                            .on_click(move |_, cx| {
+                                                call_controls::toggle_mute(&Default::default(), cx);
+                                            }),
                                         )
                                     })
-                                    .style(ButtonStyle::Subtle)
-                                    .selected_style(ButtonStyle::Tinted(TintColor::Accent))
-                                    .selected(is_shared)
-                                    .label_size(LabelSize::Small)
-                                    .on_click(cx.listener(
-                                        move |this, _, cx| {
-                                            if is_shared {
-                                                this.unshare_project(&Default::default(), cx);
+                                    .child(
+                                        IconButton::new(
+                                            "mute-sound",
+                                            if is_deafened {
+                                                ui::IconName::AudioOff
                                             } else {
-                                                this.share_project(&Default::default(), cx);
-                                            }
-                                        },
-                                    )),
-                                )
-                            },
-                        )
-                        .child(
-                            div()
-                                .child(
-                                    IconButton::new("leave-call", ui::IconName::Exit)
+                                                ui::IconName::AudioOn
+                                            },
+                                        )
                                         .style(ButtonStyle::Subtle)
-                                        .tooltip(|cx| Tooltip::text("Leave call", cx))
+                                        .selected_style(ButtonStyle::Tinted(TintColor::Negative))
                                         .icon_size(IconSize::Small)
+                                        .selected(is_deafened)
+                                        .disabled(!platform_supported)
+                                        .tooltip(move |cx| {
+                                            if !platform_supported {
+                                                Tooltip::text("Cannot share microphone", cx)
+                                            } else if can_use_microphone {
+                                                Tooltip::with_meta(
+                                                    "Deafen Audio",
+                                                    None,
+                                                    "Mic will be muted",
+                                                    cx,
+                                                )
+                                            } else {
+                                                Tooltip::text("Deafen Audio", cx)
+                                            }
+                                        })
                                         .on_click(move |_, cx| {
-                                            ActiveCall::global(cx)
-                                                .update(cx, |call, cx| call.hang_up(cx))
-                                                .detach_and_log_err(cx);
+                                            call_controls::toggle_deafen(&Default::default(), cx)
                                         }),
-                                )
-                                .pr_2(),
-                        )
-                        .when(can_use_microphone, |this| {
-                            this.child(
-                                IconButton::new(
-                                    "mute-microphone",
-                                    if is_muted {
-                                        ui::IconName::MicMute
-                                    } else {
-                                        ui::IconName::Mic
-                                    },
-                                )
-                                .tooltip(move |cx| {
-                                    Tooltip::text(
-                                        if !platform_supported {
-                                            "Cannot share microphone"
-                                        } else if is_muted {
-                                            "Unmute microphone"
-                                        } else {
-                                            "Mute microphone"
-                                        },
-                                        cx,
-                                    )
-                                })
-                                .style(ButtonStyle::Subtle)
-                                .icon_size(IconSize::Small)
-                                .selected(platform_supported && is_muted)
-                                .disabled(!platform_supported)
-                                .selected_style(ButtonStyle::Tinted(TintColor::Negative))
-                                .on_click(move |_, cx| crate::toggle_mute(&Default::default(), cx)),
-                            )
-                        })
-                        .child(
-                            IconButton::new(
-                                "mute-sound",
-                                if is_deafened {
-                                    ui::IconName::AudioOff
-                                } else {
-                                    ui::IconName::AudioOn
-                                },
-                            )
-                            .style(ButtonStyle::Subtle)
-                            .selected_style(ButtonStyle::Tinted(TintColor::Negative))
-                            .icon_size(IconSize::Small)
-                            .selected(is_deafened)
-                            .disabled(!platform_supported)
-                            .tooltip(move |cx| {
-                                if !platform_supported {
-                                    Tooltip::text("Cannot share microphone", cx)
-                                } else if can_use_microphone {
-                                    Tooltip::with_meta(
-                                        "Deafen Audio",
-                                        None,
-                                        "Mic will be muted",
-                                        cx,
                                     )
-                                } else {
-                                    Tooltip::text("Deafen Audio", cx)
-                                }
-                            })
-                            .on_click(move |_, cx| crate::toggle_deafen(&Default::default(), cx)),
-                        )
-                        .when(can_share_projects, |this| {
-                            this.child(
-                                IconButton::new("screen-share", ui::IconName::Screen)
-                                    .style(ButtonStyle::Subtle)
-                                    .icon_size(IconSize::Small)
-                                    .selected(is_screen_sharing)
-                                    .disabled(!platform_supported)
-                                    .selected_style(ButtonStyle::Tinted(TintColor::Accent))
-                                    .tooltip(move |cx| {
-                                        Tooltip::text(
-                                            if !platform_supported {
-                                                "Cannot share screen"
-                                            } else if is_screen_sharing {
-                                                "Stop Sharing Screen"
-                                            } else {
-                                                "Share Screen"
-                                            },
-                                            cx,
+                                    .when(can_share_projects, |this| {
+                                        this.child(
+                                            IconButton::new("screen-share", ui::IconName::Screen)
+                                                .style(ButtonStyle::Subtle)
+                                                .icon_size(IconSize::Small)
+                                                .selected(is_screen_sharing)
+                                                .disabled(!platform_supported)
+                                                .selected_style(ButtonStyle::Tinted(TintColor::Accent))
+                                                .tooltip(move |cx| {
+                                                    Tooltip::text(
+                                                        if !platform_supported {
+                                                            "Cannot share screen"
+                                                        } else if is_screen_sharing {
+                                                            "Stop Sharing Screen"
+                                                        } else {
+                                                            "Share Screen"
+                                                        },
+                                                        cx,
+                                                    )
+                                                })
+                                                .on_click(move |_, cx| {
+                                                    call_controls::toggle_screen_sharing(&Default::default(), cx)
+                                                }),
                                         )
                                     })
-                                    .on_click(move |_, cx| {
-                                        crate::toggle_screen_sharing(&Default::default(), cx)
-                                    }),
-                            )
+                                    .child(div().pr_2())
+                                })
+                                .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))
+                                    }
+                                }),
+                        )
+            )
+            .when(
+                self.platform_style == PlatformStyle::Windows && !cx.is_fullscreen(),
+                |title_bar| title_bar.child(platform_windows::WindowsWindowControls::new(height)),
+            )
+            .when(
+                self.platform_style == PlatformStyle::Linux
+                    && !cx.is_fullscreen()
+                    && cx.should_render_window_controls(),
+                |title_bar| {
+                    title_bar
+                        .child(platform_linux::LinuxWindowControls::new(height, close_action))
+                        .on_mouse_down(gpui::MouseButton::Right, move |ev, cx| {
+                            cx.show_window_menu(ev.position)
                         })
-                        .child(div().pr_2())
-                    })
-                    .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))
-                        }
-                    }),
+                        .on_mouse_move(move |ev, cx| {
+                            if ev.dragging() {
+                                cx.start_system_move();
+                            }
+                        })
+                },
             )
     }
 }
 
-fn render_color_ribbon(color: Hsla) -> impl Element {
-    canvas(
-        move |_, _| {},
-        move |bounds, _, cx| {
-            let height = bounds.size.height;
-            let horizontal_offset = height;
-            let vertical_offset = px(height.0 / 2.0);
-            let mut path = Path::new(bounds.lower_left());
-            path.curve_to(
-                bounds.origin + point(horizontal_offset, vertical_offset),
-                bounds.origin + point(px(0.0), vertical_offset),
-            );
-            path.line_to(bounds.upper_right() + point(-horizontal_offset, vertical_offset));
-            path.curve_to(
-                bounds.lower_right(),
-                bounds.upper_right() + point(px(0.0), vertical_offset),
-            );
-            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 {
+impl TitleBar {
+    pub fn new(
+        id: impl Into<ElementId>,
+        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();
@@ -380,6 +418,9 @@ impl CollabTitlebarItem {
         subscriptions.push(cx.observe(&user_store, |_, _, cx| cx.notify()));
 
         Self {
+            platform_style: PlatformStyle::platform(),
+            content: div().id(id.into()),
+            children: SmallVec::new(),
             workspace: workspace.weak_handle(),
             project,
             user_store,
@@ -388,6 +429,45 @@ impl CollabTitlebarItem {
         }
     }
 
+    #[cfg(not(target_os = "windows"))]
+    pub fn height(cx: &mut WindowContext) -> Pixels {
+        (1.75 * cx.rem_size()).max(px(34.))
+    }
+
+    #[cfg(target_os = "windows")]
+    pub fn height(_cx: &mut WindowContext) -> Pixels {
+        // todo(windows) instead of hard coded size report the actual size to the Windows platform API
+        px(32.)
+    }
+
+    #[cfg(not(target_os = "windows"))]
+    fn top_padding(_cx: &WindowContext) -> Pixels {
+        px(0.)
+    }
+
+    #[cfg(target_os = "windows")]
+    fn top_padding(cx: &WindowContext) -> Pixels {
+        use windows::Win32::UI::{
+            HiDpi::GetSystemMetricsForDpi,
+            WindowsAndMessaging::{SM_CXPADDEDBORDER, USER_DEFAULT_SCREEN_DPI},
+        };
+
+        // This top padding is not dependent on the title bar style and is instead a quirk of maximized windows on Windows:
+        // https://devblogs.microsoft.com/oldnewthing/20150304-00/?p=44543
+        let padding = unsafe { GetSystemMetricsForDpi(SM_CXPADDEDBORDER, USER_DEFAULT_SCREEN_DPI) };
+        if cx.is_maximized() {
+            px((padding * 2) as f32)
+        } else {
+            px(0.)
+        }
+    }
+
+    /// Sets the platform style.
+    pub fn platform_style(mut self, style: PlatformStyle) -> Self {
+        self.platform_style = style;
+        self
+    }
+
     pub fn render_application_menu(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
         cfg!(not(target_os = "macos")).then(|| {
             let ui_font_size = ThemeSettings::get_global(cx).ui_font_size;
@@ -712,97 +792,6 @@ impl CollabTitlebarItem {
         )
     }
 
-    #[allow(clippy::too_many_arguments)]
-    fn render_collaborator(
-        &self,
-        user: &Arc<User>,
-        peer_id: PeerId,
-        is_present: bool,
-        is_speaking: bool,
-        is_muted: bool,
-        leader_selection_color: Option<Hsla>,
-        room: &Room,
-        project_id: Option<u64>,
-        current_user: &Arc<User>,
-        cx: &ViewContext<Self>,
-    ) -> Option<Div> {
-        if room.role_for_user(user.id) == Some(proto::ChannelRole::Guest) {
-            return None;
-        }
-
-        const FACEPILE_LIMIT: usize = 3;
-        let followers = project_id.map_or(&[] as &[_], |id| room.followers_for(peer_id, id));
-        let extra_count = followers.len().saturating_sub(FACEPILE_LIMIT);
-
-        Some(
-            div()
-                .m_0p5()
-                .p_0p5()
-                // When the collaborator is not followed, still draw this wrapper div, but leave
-                // it transparent, so that it does not shift the layout when following.
-                .when_some(leader_selection_color, |div, color| {
-                    div.rounded_md().bg(color)
-                })
-                .child(
-                    FacePile::empty()
-                        .child(
-                            Avatar::new(user.avatar_uri.clone())
-                                .grayscale(!is_present)
-                                .border_color(if is_speaking {
-                                    cx.theme().status().info
-                                } else {
-                                    // We draw the border in a transparent color rather to avoid
-                                    // the layout shift that would come with adding/removing the border.
-                                    gpui::transparent_black()
-                                })
-                                .when(is_muted, |avatar| {
-                                    avatar.indicator(
-                                        AvatarAudioStatusIndicator::new(ui::AudioStatus::Muted)
-                                            .tooltip({
-                                                let github_login = user.github_login.clone();
-                                                move |cx| {
-                                                    Tooltip::text(
-                                                        format!("{} is muted", github_login),
-                                                        cx,
-                                                    )
-                                                }
-                                            }),
-                                    )
-                                }),
-                        )
-                        .children(followers.iter().take(FACEPILE_LIMIT).filter_map(
-                            |follower_peer_id| {
-                                let follower = room
-                                    .remote_participants()
-                                    .values()
-                                    .find_map(|p| {
-                                        (p.peer_id == *follower_peer_id).then_some(&p.user)
-                                    })
-                                    .or_else(|| {
-                                        (self.client.peer_id() == Some(*follower_peer_id))
-                                            .then_some(current_user)
-                                    })?
-                                    .clone();
-
-                                Some(div().mt(-px(4.)).child(
-                                    Avatar::new(follower.avatar_uri.clone()).size(rems(0.75)),
-                                ))
-                            },
-                        ))
-                        .children(if extra_count > 0 {
-                            Some(
-                                div()
-                                    .ml_1()
-                                    .child(Label::new(format!("+{extra_count}")))
-                                    .into_any_element(),
-                            )
-                        } else {
-                            None
-                        }),
-                ),
-        )
-    }
-
     fn window_activation_changed(&mut self, cx: &mut ViewContext<Self>) {
         if cx.is_window_active() {
             ActiveCall::global(cx)
@@ -960,3 +949,17 @@ impl CollabTitlebarItem {
         }
     }
 }
+
+impl InteractiveElement for TitleBar {
+    fn interactivity(&mut self) -> &mut Interactivity {
+        self.content.interactivity()
+    }
+}
+
+impl StatefulInteractiveElement for TitleBar {}
+
+impl ParentElement for TitleBar {
+    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
+        self.children.extend(elements)
+    }
+}

crates/ui/src/components.rs πŸ”—

@@ -5,6 +5,7 @@ mod context_menu;
 mod disclosure;
 mod divider;
 mod dropdown_menu;
+mod facepile;
 mod icon;
 mod indicator;
 mod keybinding;
@@ -19,7 +20,6 @@ mod setting;
 mod stack;
 mod tab;
 mod tab_bar;
-mod title_bar;
 mod tool_strip;
 mod tooltip;
 
@@ -33,6 +33,7 @@ pub use context_menu::*;
 pub use disclosure::*;
 pub use divider::*;
 use dropdown_menu::*;
+pub use facepile::*;
 pub use icon::*;
 pub use indicator::*;
 pub use keybinding::*;
@@ -47,7 +48,6 @@ pub use setting::*;
 pub use stack::*;
 pub use tab::*;
 pub use tab_bar::*;
-pub use title_bar::*;
 pub use tool_strip::*;
 pub use tooltip::*;
 

crates/collab_ui/src/face_pile.rs β†’ crates/ui/src/components/facepile.rs πŸ”—

@@ -1,14 +1,19 @@
+use crate::prelude::*;
 use gpui::AnyElement;
 use smallvec::SmallVec;
-use ui::prelude::*;
 
+/// A facepile is a collection of faces stacked horizontally–
+/// always with the leftmost face on top and descending in z-index
+///
+/// Facepiles are used to display a group of people or things,
+/// such as a list of participants in a collaboration session.
 #[derive(IntoElement)]
-pub struct FacePile {
+pub struct Facepile {
     base: Div,
     faces: SmallVec<[AnyElement; 2]>,
 }
 
-impl FacePile {
+impl Facepile {
     pub fn empty() -> Self {
         Self::new(SmallVec::new())
     }
@@ -18,7 +23,7 @@ impl FacePile {
     }
 }
 
-impl RenderOnce for FacePile {
+impl RenderOnce for Facepile {
     fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
         // Lay the faces out in reverse so they overlap in the desired order (left to right, front to back)
         self.base
@@ -36,13 +41,13 @@ impl RenderOnce for FacePile {
     }
 }
 
-impl ParentElement for FacePile {
+impl ParentElement for Facepile {
     fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
         self.faces.extend(elements);
     }
 }
 
-impl Styled for FacePile {
+impl Styled for Facepile {
     fn style(&mut self) -> &mut gpui::StyleRefinement {
         self.base.style()
     }

crates/ui/src/components/stories.rs πŸ”—

@@ -13,7 +13,6 @@ mod list_item;
 mod setting;
 mod tab;
 mod tab_bar;
-mod title_bar;
 mod toggle_button;
 mod tool_strip;
 
@@ -32,6 +31,5 @@ pub use list_item::*;
 pub use setting::*;
 pub use tab::*;
 pub use tab_bar::*;
-pub use title_bar::*;
 pub use toggle_button::*;
 pub use tool_strip::*;

crates/ui/src/components/title_bar/title_bar.rs πŸ”—

@@ -1,135 +0,0 @@
-use gpui::{Action, AnyElement, Interactivity, Stateful};
-use smallvec::SmallVec;
-
-use crate::components::title_bar::linux_window_controls::LinuxWindowControls;
-use crate::components::title_bar::windows_window_controls::WindowsWindowControls;
-use crate::prelude::*;
-
-#[derive(IntoElement)]
-pub struct TitleBar {
-    platform_style: PlatformStyle,
-    content: Stateful<Div>,
-    children: SmallVec<[AnyElement; 2]>,
-    close_window_action: Box<dyn Action>,
-}
-
-impl TitleBar {
-    #[cfg(not(target_os = "windows"))]
-    pub fn height(cx: &mut WindowContext) -> Pixels {
-        (1.75 * cx.rem_size()).max(px(34.))
-    }
-
-    #[cfg(target_os = "windows")]
-    pub fn height(_cx: &mut WindowContext) -> Pixels {
-        // todo(windows) instead of hard coded size report the actual size to the Windows platform API
-        px(32.)
-    }
-
-    #[cfg(not(target_os = "windows"))]
-    fn top_padding(_cx: &WindowContext) -> Pixels {
-        px(0.)
-    }
-
-    #[cfg(target_os = "windows")]
-    fn top_padding(cx: &WindowContext) -> Pixels {
-        use windows::Win32::UI::{
-            HiDpi::GetSystemMetricsForDpi,
-            WindowsAndMessaging::{SM_CXPADDEDBORDER, USER_DEFAULT_SCREEN_DPI},
-        };
-
-        // This top padding is not dependent on the title bar style and is instead a quirk of maximized windows on Windows:
-        // https://devblogs.microsoft.com/oldnewthing/20150304-00/?p=44543
-        let padding = unsafe { GetSystemMetricsForDpi(SM_CXPADDEDBORDER, USER_DEFAULT_SCREEN_DPI) };
-        if cx.is_maximized() {
-            px((padding * 2) as f32)
-        } else {
-            px(0.)
-        }
-    }
-
-    pub fn new(id: impl Into<ElementId>, close_window_action: Box<dyn Action>) -> Self {
-        Self {
-            platform_style: PlatformStyle::platform(),
-            content: div().id(id.into()),
-            children: SmallVec::new(),
-            close_window_action,
-        }
-    }
-
-    /// Sets the platform style.
-    pub fn platform_style(mut self, style: PlatformStyle) -> Self {
-        self.platform_style = style;
-        self
-    }
-}
-
-impl InteractiveElement for TitleBar {
-    fn interactivity(&mut self) -> &mut Interactivity {
-        self.content.interactivity()
-    }
-}
-
-impl StatefulInteractiveElement for TitleBar {}
-
-impl ParentElement for TitleBar {
-    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
-        self.children.extend(elements)
-    }
-}
-
-impl RenderOnce for TitleBar {
-    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
-        let height = Self::height(cx);
-        h_flex()
-            .id("titlebar")
-            .w_full()
-            .pt(Self::top_padding(cx))
-            .h(height + Self::top_padding(cx))
-            .map(|this| {
-                if cx.is_fullscreen() {
-                    this.pl_2()
-                } else if self.platform_style == PlatformStyle::Mac {
-                    // 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.
-                    //
-                    // Magic number: There is one extra pixel of padding on the left side due to
-                    // the 1px border around the window on macOS apps.
-                    this.pl(px(71.))
-                } else {
-                    this.pl_2()
-                }
-            })
-            .bg(cx.theme().colors().title_bar_background)
-            .content_stretch()
-            .child(
-                self.content
-                    .id("titlebar-content")
-                    .flex()
-                    .flex_row()
-                    .justify_between()
-                    .w_full()
-                    .children(self.children),
-            )
-            .when(
-                self.platform_style == PlatformStyle::Windows && !cx.is_fullscreen(),
-                |title_bar| title_bar.child(WindowsWindowControls::new(height)),
-            )
-            .when(
-                self.platform_style == PlatformStyle::Linux
-                    && !cx.is_fullscreen()
-                    && cx.should_render_window_controls(),
-                |title_bar| {
-                    title_bar
-                        .child(LinuxWindowControls::new(height, self.close_window_action))
-                        .on_mouse_down(gpui::MouseButton::Right, move |ev, cx| {
-                            cx.show_window_menu(ev.position)
-                        })
-                        .on_mouse_move(move |ev, cx| {
-                            if ev.dragging() {
-                                cx.start_system_move();
-                            }
-                        })
-                },
-            )
-    }
-}