Extract title_bar crate (#13597)

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

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

Release Notes:

- N/A
This commit is contained in:
Nate Butler 2024-06-27 19:14:13 -04:00 committed by GitHub
parent 7652a8ae23
commit 0b57df5deb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 756 additions and 655 deletions

44
Cargo.lock generated
View File

@ -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"

View File

@ -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" }

View File

@ -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()

View File

@ -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"] }

View File

@ -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())

View File

@ -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(

View File

@ -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(),

View File

@ -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"] }

View File

@ -0,0 +1 @@
../../LICENSE-GPL

View File

@ -0,0 +1 @@
../../LICENSE_-GPL

View File

@ -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));
}
}

View File

@ -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
}),
),
)
}
}

View File

@ -0,0 +1,3 @@
pub mod platform_linux;
pub mod platform_mac;
pub mod platform_windows;

View File

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

View File

@ -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.;

View File

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

View File

@ -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),
)

View File

@ -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");
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::<Vec<_>>();
remote_participants.sort_by_key(|p| p.participant_index.0);
let current_user_face_pile = self.render_collaborator(
&current_user,
peer_id,
true,
room.is_speaking(),
room.is_muted(),
None,
&room,
project_id,
&current_user,
cx,
);
this.children(current_user_face_pile.map(|face_pile| {
v_flex()
.on_mouse_move(|_, cx| cx.stop_propagation())
.child(face_pile)
.child(render_color_ribbon(player_colors.local().cursor))
}))
.children(
remote_participants.iter().filter_map(|collaborator| {
let player_color = player_colors
.color_for_participant(collaborator.participant_index.0);
let is_following = workspace
.as_ref()?
.read(cx)
.is_being_followed(collaborator.peer_id);
let is_present = project_id.map_or(false, |project_id| {
collaborator.location
== ParticipantLocation::SharedProject { project_id }
});
let face_pile = self.render_collaborator(
&collaborator.user,
collaborator.peer_id,
is_present,
collaborator.speaking,
collaborator.muted,
is_following.then_some(player_color.selection),
&room,
project_id,
&current_user,
cx,
)?;
Some(
v_flex()
.id(("collaborator", collaborator.user.id))
.child(face_pile)
.child(render_color_ribbon(player_color.cursor))
.cursor_pointer()
.on_click({
let peer_id = collaborator.peer_id;
cx.listener(move |this, _, cx| {
this.workspace
.update(cx, |workspace, cx| {
workspace.follow(peer_id, cx);
})
.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::<Vec<_>>();
remote_participants.sort_by_key(|p| p.participant_index.0);
let current_user_face_pile = self.render_collaborator(
&current_user,
peer_id,
true,
room.is_speaking(),
room.is_muted(),
None,
&room,
project_id,
&current_user,
cx,
);
this.children(current_user_face_pile.map(|face_pile| {
v_flex()
.on_mouse_move(|_, cx| cx.stop_propagation())
.child(face_pile)
.child(render_color_ribbon(player_colors.local().cursor))
}))
.children(
remote_participants.iter().filter_map(|collaborator| {
let player_color = player_colors
.color_for_participant(collaborator.participant_index.0);
let is_following = workspace
.as_ref()?
.read(cx)
.is_being_followed(collaborator.peer_id);
let is_present = project_id.map_or(false, |project_id| {
collaborator.location
== ParticipantLocation::SharedProject { project_id }
});
let facepile = self.render_collaborator(
&collaborator.user,
collaborator.peer_id,
is_present,
collaborator.speaking,
collaborator.muted,
is_following.then_some(player_color.selection),
&room,
project_id,
&current_user,
cx,
)?;
Some(
v_flex()
.id(("collaborator", collaborator.user.id))
.child(facepile)
.child(render_color_ribbon(player_color.cursor))
.cursor_pointer()
.on_click({
let peer_id = collaborator.peer_id;
cx.listener(move |this, _, cx| {
this.workspace
.update(cx, |workspace, cx| {
workspace.follow(peer_id, cx);
})
.ok();
})
})
.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>) -> 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)
}
}

View File

@ -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::*;

View File

@ -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()
}

View File

@ -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::*;

View File

@ -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();
}
})
},
)
}
}