diff --git a/Cargo.lock b/Cargo.lock index c565aa3a83..4e29aa2666 100644 --- a/Cargo.lock +++ b/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" diff --git a/Cargo.toml b/Cargo.toml index 3cc5b8fcc1..d034661f9e 100644 --- a/Cargo.toml +++ b/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" } diff --git a/crates/assistant/src/prompt_library.rs b/crates/assistant/src/prompt_library.rs index 67634fb3c4..b8cfc30c2d 100644 --- a/crates/assistant/src/prompt_library.rs +++ b/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() diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index c1715c68e0..305a10d5f3 100644 --- a/crates/collab_ui/Cargo.toml +++ b/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"] } diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 6cdc4d484d..5d9b88377e 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/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()) diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index 8dc1cff95c..fe177603cb 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/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, cx: &mut AppContext) { CollaborationPanelSettings::register(cx); @@ -36,63 +27,13 @@ pub fn init(app_state: &Arc, 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( diff --git a/crates/storybook/src/story_selector.rs b/crates/storybook/src/story_selector.rs index 46a79ccc6a..ce0ab26a1e 100644 --- a/crates/storybook/src/story_selector.rs +++ b/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(), diff --git a/crates/title_bar/Cargo.toml b/crates/title_bar/Cargo.toml new file mode 100644 index 0000000000..5e5bd754e5 --- /dev/null +++ b/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"] } diff --git a/crates/title_bar/LICENSE-GPL b/crates/title_bar/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/title_bar/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/title_bar/LICENSE_-GPL b/crates/title_bar/LICENSE_-GPL new file mode 120000 index 0000000000..84d5e8cf9c --- /dev/null +++ b/crates/title_bar/LICENSE_-GPL @@ -0,0 +1 @@ +../../LICENSE_-GPL \ No newline at end of file diff --git a/crates/title_bar/src/call_controls.rs b/crates/title_bar/src/call_controls.rs new file mode 100644 index 0000000000..107efc8ed2 --- /dev/null +++ b/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)); + } +} diff --git a/crates/title_bar/src/collab.rs b/crates/title_bar/src/collab.rs new file mode 100644 index 0000000000..992ac35836 --- /dev/null +++ b/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, + peer_id: PeerId, + is_present: bool, + is_speaking: bool, + is_muted: bool, + leader_selection_color: Option, + room: &Room, + project_id: Option, + current_user: &Arc, + cx: &ViewContext, + ) -> Option
{ + 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 + }), + ), + ) + } +} diff --git a/crates/title_bar/src/platforms.rs b/crates/title_bar/src/platforms.rs new file mode 100644 index 0000000000..67e87d45ea --- /dev/null +++ b/crates/title_bar/src/platforms.rs @@ -0,0 +1,3 @@ +pub mod platform_linux; +pub mod platform_mac; +pub mod platform_windows; diff --git a/crates/ui/src/components/title_bar/linux_window_controls.rs b/crates/title_bar/src/platforms/platform_linux.rs similarity index 99% rename from crates/ui/src/components/title_bar/linux_window_controls.rs rename to crates/title_bar/src/platforms/platform_linux.rs index a3cbebbcf1..35908b0b7d 100644 --- a/crates/ui/src/components/title_bar/linux_window_controls.rs +++ b/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 { diff --git a/crates/title_bar/src/platforms/platform_mac.rs b/crates/title_bar/src/platforms/platform_mac.rs new file mode 100644 index 0000000000..c7becde6c1 --- /dev/null +++ b/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.; diff --git a/crates/ui/src/components/title_bar/windows_window_controls.rs b/crates/title_bar/src/platforms/platform_windows.rs similarity index 99% rename from crates/ui/src/components/title_bar/windows_window_controls.rs rename to crates/title_bar/src/platforms/platform_windows.rs index d8d91773d2..8ddb1c1bdc 100644 --- a/crates/ui/src/components/title_bar/windows_window_controls.rs +++ b/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 { diff --git a/crates/ui/src/components/stories/title_bar.rs b/crates/title_bar/src/stories/title_bar.rs similarity index 84% rename from crates/ui/src/components/stories/title_bar.rs rename to crates/title_bar/src/stories/title_bar.rs index 254e92ecaf..99209780d3 100644 --- a/crates/ui/src/components/stories/title_bar.rs +++ b/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) -> 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), ) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/title_bar/src/title_bar.rs similarity index 55% rename from crates/collab_ui/src/collab_titlebar_item.rs rename to crates/title_bar/src/title_bar.rs index 728552d996..72ea788f9d 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/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
, + children: SmallVec<[AnyElement; 2]>, project: Model, user_store: Model, client: Arc, @@ -51,319 +60,348 @@ pub struct CollabTitlebarItem { _subscriptions: Vec, } -impl Render for CollabTitlebarItem { +impl Render for TitleBar { fn render(&mut self, cx: &mut ViewContext) -> 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"); + let height = Self::height(cx); - 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(); - } - }) + 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::>(); - remote_participants.sort_by_key(|p| p.participant_index.0); - - let current_user_face_pile = self.render_collaborator( - ¤t_user, - peer_id, - true, - room.is_speaking(), - room.is_muted(), - None, - &room, - project_id, - ¤t_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, - ¤t_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); - }) - .ok(); - }) - }) - .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); - } - }, - )), - ) - }, - ) - .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(), - ) - .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)), - ) + // 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(); + } }) - .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, - ) - }) - .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)) - } - }), + // 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::>(); + remote_participants.sort_by_key(|p| p.participant_index.0); + + let current_user_face_pile = self.render_collaborator( + ¤t_user, + peer_id, + true, + room.is_speaking(), + room.is_muted(), + None, + &room, + project_id, + ¤t_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, + ¤t_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(); + }) + }) + .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); + } + }, + )), + ) + }, + ) + .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(), + ) + .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); + }), + ) + }) + .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| { + call_controls::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, + ) + }) + .on_click(move |_, cx| { + call_controls::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) + }) + .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 { +impl TitleBar { + pub fn new( + id: impl Into, + workspace: &Workspace, + cx: &mut ViewContext, + ) -> 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) -> Option { 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, - peer_id: PeerId, - is_present: bool, - is_speaking: bool, - is_muted: bool, - leader_selection_color: Option, - room: &Room, - project_id: Option, - current_user: &Arc, - cx: &ViewContext, - ) -> Option
{ - 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) { 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) { + self.children.extend(elements) + } +} diff --git a/crates/ui/src/components.rs b/crates/ui/src/components.rs index 1b33ec420c..1ceaf43487 100644 --- a/crates/ui/src/components.rs +++ b/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::*; diff --git a/crates/collab_ui/src/face_pile.rs b/crates/ui/src/components/facepile.rs similarity index 72% rename from crates/collab_ui/src/face_pile.rs rename to crates/ui/src/components/facepile.rs index bce52f090e..6aa8a46aa4 100644 --- a/crates/collab_ui/src/face_pile.rs +++ b/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) { self.faces.extend(elements); } } -impl Styled for FacePile { +impl Styled for Facepile { fn style(&mut self) -> &mut gpui::StyleRefinement { self.base.style() } diff --git a/crates/ui/src/components/stories.rs b/crates/ui/src/components/stories.rs index 81b08762eb..4007fba998 100644 --- a/crates/ui/src/components/stories.rs +++ b/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::*; diff --git a/crates/ui/src/components/title_bar/title_bar.rs b/crates/ui/src/components/title_bar/title_bar.rs deleted file mode 100644 index f80a4648f8..0000000000 --- a/crates/ui/src/components/title_bar/title_bar.rs +++ /dev/null @@ -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
, - children: SmallVec<[AnyElement; 2]>, - close_window_action: Box, -} - -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, close_window_action: Box) -> 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) { - 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(); - } - }) - }, - ) - } -}