Detailed changes
@@ -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"
@@ -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" }
@@ -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()
@@ -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"] }
@@ -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())
@@ -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(
@@ -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(),
@@ -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"] }
@@ -0,0 +1 @@
+../../LICENSE-GPL
@@ -0,0 +1 @@
+../../LICENSE_-GPL
@@ -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));
+ }
+}
@@ -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
+ }),
+ ),
+ )
+ }
+}
@@ -0,0 +1,3 @@
+pub mod platform_linux;
+pub mod platform_mac;
+pub mod platform_windows;
@@ -1,6 +1,6 @@
use gpui::{prelude::*, Action, Rgba, WindowAppearance};
-use crate::prelude::*;
+use ui::prelude::*;
#[derive(IntoElement)]
pub struct LinuxWindowControls {
@@ -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.;
@@ -1,6 +1,6 @@
use gpui::{prelude::*, Rgba, WindowAppearance};
-use crate::prelude::*;
+use ui::prelude::*;
#[derive(IntoElement)]
pub struct WindowsWindowControls {
@@ -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),
)
@@ -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(
- ¤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);
+ // 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(
+ ¤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();
+ })
})
- .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)
+ }
+}
@@ -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::*;
@@ -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()
}
@@ -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::*;
@@ -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();
- }
- })
- },
- )
- }
-}