1mod application_menu;
2mod collab;
3mod onboarding_banner;
4mod platforms;
5mod title_bar_settings;
6mod window_controls;
7
8#[cfg(feature = "stories")]
9mod stories;
10
11use crate::application_menu::ApplicationMenu;
12
13#[cfg(not(target_os = "macos"))]
14use crate::application_menu::{
15 ActivateDirection, ActivateMenuLeft, ActivateMenuRight, OpenApplicationMenu,
16};
17
18use crate::platforms::{platform_linux, platform_mac, platform_windows};
19use auto_update::AutoUpdateStatus;
20use call::ActiveCall;
21use client::{Client, UserStore};
22use gpui::{
23 Action, AnyElement, App, Context, Corner, Decorations, Element, Entity, InteractiveElement,
24 Interactivity, IntoElement, MouseButton, ParentElement, Render, Stateful,
25 StatefulInteractiveElement, Styled, Subscription, WeakEntity, Window, actions, div, px,
26};
27use onboarding_banner::OnboardingBanner;
28use project::Project;
29use rpc::proto;
30use settings::Settings as _;
31use smallvec::SmallVec;
32use std::sync::Arc;
33use theme::ActiveTheme;
34use title_bar_settings::TitleBarSettings;
35use ui::{
36 Avatar, Button, ButtonLike, ButtonStyle, ContextMenu, Icon, IconName, IconSize,
37 IconWithIndicator, Indicator, PopoverMenu, Tooltip, h_flex, prelude::*,
38};
39use util::ResultExt;
40use workspace::{Workspace, notifications::NotifyResultExt};
41use zed_actions::{OpenRecent, OpenRemote};
42
43pub use onboarding_banner::restore_banner;
44
45#[cfg(feature = "stories")]
46pub use stories::*;
47
48const MAX_PROJECT_NAME_LENGTH: usize = 40;
49const MAX_BRANCH_NAME_LENGTH: usize = 40;
50const MAX_SHORT_SHA_LENGTH: usize = 8;
51
52actions!(collab, [ToggleUserMenu, ToggleProjectMenu, SwitchBranch]);
53
54pub fn init(cx: &mut App) {
55 TitleBarSettings::register(cx);
56
57 cx.observe_new(|workspace: &mut Workspace, window, cx| {
58 let Some(window) = window else {
59 return;
60 };
61 let item = cx.new(|cx| TitleBar::new("title-bar", workspace, window, cx));
62 workspace.set_titlebar_item(item.into(), window, cx);
63
64 #[cfg(not(target_os = "macos"))]
65 workspace.register_action(|workspace, action: &OpenApplicationMenu, window, cx| {
66 if let Some(titlebar) = workspace
67 .titlebar_item()
68 .and_then(|item| item.downcast::<TitleBar>().ok())
69 {
70 titlebar.update(cx, |titlebar, cx| {
71 if let Some(ref menu) = titlebar.application_menu {
72 menu.update(cx, |menu, cx| menu.open_menu(action, window, cx));
73 }
74 });
75 }
76 });
77
78 #[cfg(not(target_os = "macos"))]
79 workspace.register_action(|workspace, _: &ActivateMenuRight, window, cx| {
80 if let Some(titlebar) = workspace
81 .titlebar_item()
82 .and_then(|item| item.downcast::<TitleBar>().ok())
83 {
84 titlebar.update(cx, |titlebar, cx| {
85 if let Some(ref menu) = titlebar.application_menu {
86 menu.update(cx, |menu, cx| {
87 menu.navigate_menus_in_direction(ActivateDirection::Right, window, cx)
88 });
89 }
90 });
91 }
92 });
93
94 #[cfg(not(target_os = "macos"))]
95 workspace.register_action(|workspace, _: &ActivateMenuLeft, window, cx| {
96 if let Some(titlebar) = workspace
97 .titlebar_item()
98 .and_then(|item| item.downcast::<TitleBar>().ok())
99 {
100 titlebar.update(cx, |titlebar, cx| {
101 if let Some(ref menu) = titlebar.application_menu {
102 menu.update(cx, |menu, cx| {
103 menu.navigate_menus_in_direction(ActivateDirection::Left, window, cx)
104 });
105 }
106 });
107 }
108 });
109 })
110 .detach();
111}
112
113pub struct TitleBar {
114 platform_style: PlatformStyle,
115 content: Stateful<Div>,
116 children: SmallVec<[AnyElement; 2]>,
117 project: Entity<Project>,
118 user_store: Entity<UserStore>,
119 client: Arc<Client>,
120 workspace: WeakEntity<Workspace>,
121 should_move: bool,
122 application_menu: Option<Entity<ApplicationMenu>>,
123 _subscriptions: Vec<Subscription>,
124 banner: Entity<OnboardingBanner>,
125}
126
127impl Render for TitleBar {
128 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
129 let title_bar_settings = *TitleBarSettings::get_global(cx);
130 let close_action = Box::new(workspace::CloseWindow);
131 let height = Self::height(window);
132 let supported_controls = window.window_controls();
133 let decorations = window.window_decorations();
134 let titlebar_color = if cfg!(any(target_os = "linux", target_os = "freebsd")) {
135 if window.is_window_active() && !self.should_move {
136 cx.theme().colors().title_bar_background
137 } else {
138 cx.theme().colors().title_bar_inactive_background
139 }
140 } else {
141 cx.theme().colors().title_bar_background
142 };
143
144 h_flex()
145 .id("titlebar")
146 .w_full()
147 .h(height)
148 .map(|this| {
149 if window.is_fullscreen() {
150 this.pl_2()
151 } else if self.platform_style == PlatformStyle::Mac {
152 this.pl(px(platform_mac::TRAFFIC_LIGHT_PADDING))
153 } else {
154 this.pl_2()
155 }
156 })
157 .map(|el| match decorations {
158 Decorations::Server => el,
159 Decorations::Client { tiling, .. } => el
160 .when(!(tiling.top || tiling.right), |el| {
161 el.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
162 })
163 .when(!(tiling.top || tiling.left), |el| {
164 el.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
165 })
166 // this border is to avoid a transparent gap in the rounded corners
167 .mt(px(-1.))
168 .border(px(1.))
169 .border_color(titlebar_color),
170 })
171 .bg(titlebar_color)
172 .content_stretch()
173 .child(
174 div()
175 .id("titlebar-content")
176 .flex()
177 .flex_row()
178 .items_center()
179 .justify_between()
180 .w_full()
181 // Note: On Windows the title bar behavior is handled by the platform implementation.
182 .when(self.platform_style != PlatformStyle::Windows, |this| {
183 this.on_click(|event, window, _| {
184 if event.up.click_count == 2 {
185 window.zoom_window();
186 }
187 })
188 })
189 .child(
190 h_flex()
191 .gap_1()
192 .map(|title_bar| {
193 let mut render_project_items = title_bar_settings.show_branch_name
194 || title_bar_settings.show_project_items;
195 title_bar
196 .when_some(self.application_menu.clone(), |title_bar, menu| {
197 render_project_items &= !menu.read(cx).all_menus_shown();
198 title_bar.child(menu)
199 })
200 .when(render_project_items, |title_bar| {
201 title_bar
202 .when(
203 title_bar_settings.show_project_items,
204 |title_bar| {
205 title_bar
206 .children(self.render_project_host(cx))
207 .child(self.render_project_name(cx))
208 },
209 )
210 .when(
211 title_bar_settings.show_branch_name,
212 |title_bar| {
213 title_bar
214 .children(self.render_project_branch(cx))
215 },
216 )
217 })
218 })
219 .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()),
220 )
221 .child(self.render_collaborator_list(window, cx))
222 .when(title_bar_settings.show_onboarding_banner, |title_bar| {
223 title_bar.child(self.banner.clone())
224 })
225 .child(
226 h_flex()
227 .gap_1()
228 .pr_1()
229 .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
230 .children(self.render_call_controls(window, cx))
231 .map(|el| {
232 let status = self.client.status();
233 let status = &*status.borrow();
234 if matches!(status, client::Status::Connected { .. }) {
235 el.child(self.render_user_menu_button(cx))
236 } else {
237 el.children(self.render_connection_status(status, cx))
238 .when(TitleBarSettings::get_global(cx).show_sign_in, |el| {
239 el.child(self.render_sign_in_button(cx))
240 })
241 .child(self.render_user_menu_button(cx))
242 }
243 }),
244 ),
245 )
246 .when(!window.is_fullscreen(), |title_bar| {
247 match self.platform_style {
248 PlatformStyle::Mac => title_bar,
249 PlatformStyle::Linux => {
250 if matches!(decorations, Decorations::Client { .. }) {
251 title_bar
252 .child(platform_linux::LinuxWindowControls::new(close_action))
253 .when(supported_controls.window_menu, |titlebar| {
254 titlebar.on_mouse_down(
255 gpui::MouseButton::Right,
256 move |ev, window, _| window.show_window_menu(ev.position),
257 )
258 })
259 .on_mouse_move(cx.listener(move |this, _ev, window, _| {
260 if this.should_move {
261 this.should_move = false;
262 window.start_window_move();
263 }
264 }))
265 .on_mouse_down_out(cx.listener(move |this, _ev, _window, _cx| {
266 this.should_move = false;
267 }))
268 .on_mouse_up(
269 gpui::MouseButton::Left,
270 cx.listener(move |this, _ev, _window, _cx| {
271 this.should_move = false;
272 }),
273 )
274 .on_mouse_down(
275 gpui::MouseButton::Left,
276 cx.listener(move |this, _ev, _window, _cx| {
277 this.should_move = true;
278 }),
279 )
280 } else {
281 title_bar
282 }
283 }
284 PlatformStyle::Windows => {
285 title_bar.child(platform_windows::WindowsWindowControls::new(height))
286 }
287 }
288 })
289 }
290}
291
292impl TitleBar {
293 pub fn new(
294 id: impl Into<ElementId>,
295 workspace: &Workspace,
296 window: &mut Window,
297 cx: &mut Context<Self>,
298 ) -> Self {
299 let project = workspace.project().clone();
300 let user_store = workspace.app_state().user_store.clone();
301 let client = workspace.app_state().client.clone();
302 let active_call = ActiveCall::global(cx);
303
304 let platform_style = PlatformStyle::platform();
305 let application_menu = match platform_style {
306 PlatformStyle::Mac => {
307 if option_env!("ZED_USE_CROSS_PLATFORM_MENU").is_some() {
308 Some(cx.new(|cx| ApplicationMenu::new(window, cx)))
309 } else {
310 None
311 }
312 }
313 PlatformStyle::Linux | PlatformStyle::Windows => {
314 Some(cx.new(|cx| ApplicationMenu::new(window, cx)))
315 }
316 };
317
318 let mut subscriptions = Vec::new();
319 subscriptions.push(
320 cx.observe(&workspace.weak_handle().upgrade().unwrap(), |_, _, cx| {
321 cx.notify()
322 }),
323 );
324 subscriptions.push(cx.subscribe(&project, |_, _, _: &project::Event, cx| cx.notify()));
325 subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx)));
326 subscriptions.push(cx.observe_window_activation(window, Self::window_activation_changed));
327 subscriptions.push(cx.observe(&user_store, |_, _, cx| cx.notify()));
328
329 let banner = cx.new(|cx| {
330 OnboardingBanner::new(
331 "Agentic Onboarding",
332 IconName::ZedAssistant,
333 "Agentic Editing",
334 None,
335 zed_actions::agent::OpenOnboardingModal.boxed_clone(),
336 cx,
337 )
338 });
339
340 Self {
341 platform_style,
342 content: div().id(id.into()),
343 children: SmallVec::new(),
344 application_menu,
345 workspace: workspace.weak_handle(),
346 should_move: false,
347 project,
348 user_store,
349 client,
350 _subscriptions: subscriptions,
351 banner,
352 }
353 }
354
355 #[cfg(not(target_os = "windows"))]
356 pub fn height(window: &mut Window) -> Pixels {
357 (1.75 * window.rem_size()).max(px(34.))
358 }
359
360 #[cfg(target_os = "windows")]
361 pub fn height(_window: &mut Window) -> Pixels {
362 // todo(windows) instead of hard coded size report the actual size to the Windows platform API
363 px(32.)
364 }
365
366 /// Sets the platform style.
367 pub fn platform_style(mut self, style: PlatformStyle) -> Self {
368 self.platform_style = style;
369 self
370 }
371
372 fn render_ssh_project_host(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
373 let options = self.project.read(cx).ssh_connection_options(cx)?;
374 let host: SharedString = options.connection_string().into();
375
376 let nickname = options
377 .nickname
378 .clone()
379 .map(|nick| nick.into())
380 .unwrap_or_else(|| host.clone());
381
382 let (indicator_color, meta) = match self.project.read(cx).ssh_connection_state(cx)? {
383 remote::ConnectionState::Connecting => (Color::Info, format!("Connecting to: {host}")),
384 remote::ConnectionState::Connected => (Color::Success, format!("Connected to: {host}")),
385 remote::ConnectionState::HeartbeatMissed => (
386 Color::Warning,
387 format!("Connection attempt to {host} missed. Retrying..."),
388 ),
389 remote::ConnectionState::Reconnecting => (
390 Color::Warning,
391 format!("Lost connection to {host}. Reconnecting..."),
392 ),
393 remote::ConnectionState::Disconnected => {
394 (Color::Error, format!("Disconnected from {host}"))
395 }
396 };
397
398 let icon_color = match self.project.read(cx).ssh_connection_state(cx)? {
399 remote::ConnectionState::Connecting => Color::Info,
400 remote::ConnectionState::Connected => Color::Default,
401 remote::ConnectionState::HeartbeatMissed => Color::Warning,
402 remote::ConnectionState::Reconnecting => Color::Warning,
403 remote::ConnectionState::Disconnected => Color::Error,
404 };
405
406 let meta = SharedString::from(meta);
407
408 Some(
409 ButtonLike::new("ssh-server-icon")
410 .child(
411 h_flex()
412 .gap_2()
413 .max_w_32()
414 .child(
415 IconWithIndicator::new(
416 Icon::new(IconName::Server)
417 .size(IconSize::XSmall)
418 .color(icon_color),
419 Some(Indicator::dot().color(indicator_color)),
420 )
421 .indicator_border_color(Some(cx.theme().colors().title_bar_background))
422 .into_any_element(),
423 )
424 .child(
425 Label::new(nickname.clone())
426 .size(LabelSize::Small)
427 .truncate(),
428 ),
429 )
430 .tooltip(move |window, cx| {
431 Tooltip::with_meta(
432 "Remote Project",
433 Some(&OpenRemote {
434 from_existing_connection: false,
435 }),
436 meta.clone(),
437 window,
438 cx,
439 )
440 })
441 .on_click(|_, window, cx| {
442 window.dispatch_action(
443 OpenRemote {
444 from_existing_connection: false,
445 }
446 .boxed_clone(),
447 cx,
448 );
449 })
450 .into_any_element(),
451 )
452 }
453
454 pub fn render_project_host(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
455 if self.project.read(cx).is_via_ssh() {
456 return self.render_ssh_project_host(cx);
457 }
458
459 if self.project.read(cx).is_disconnected(cx) {
460 return Some(
461 Button::new("disconnected", "Disconnected")
462 .disabled(true)
463 .color(Color::Disabled)
464 .style(ButtonStyle::Subtle)
465 .label_size(LabelSize::Small)
466 .into_any_element(),
467 );
468 }
469
470 let host = self.project.read(cx).host()?;
471 let host_user = self.user_store.read(cx).get_cached_user(host.user_id)?;
472 let participant_index = self
473 .user_store
474 .read(cx)
475 .participant_indices()
476 .get(&host_user.id)?;
477 Some(
478 Button::new("project_owner_trigger", host_user.github_login.clone())
479 .color(Color::Player(participant_index.0))
480 .style(ButtonStyle::Subtle)
481 .label_size(LabelSize::Small)
482 .tooltip(Tooltip::text(format!(
483 "{} is sharing this project. Click to follow.",
484 host_user.github_login
485 )))
486 .on_click({
487 let host_peer_id = host.peer_id;
488 cx.listener(move |this, _, window, cx| {
489 this.workspace
490 .update(cx, |workspace, cx| {
491 workspace.follow(host_peer_id, window, cx);
492 })
493 .log_err();
494 })
495 })
496 .into_any_element(),
497 )
498 }
499
500 pub fn render_project_name(&self, cx: &mut Context<Self>) -> impl IntoElement {
501 let name = {
502 let mut names = self.project.read(cx).visible_worktrees(cx).map(|worktree| {
503 let worktree = worktree.read(cx);
504 worktree.root_name()
505 });
506
507 names.next()
508 };
509 let is_project_selected = name.is_some();
510 let name = if let Some(name) = name {
511 util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH)
512 } else {
513 "Open recent project".to_string()
514 };
515
516 Button::new("project_name_trigger", name)
517 .when(!is_project_selected, |b| b.color(Color::Muted))
518 .style(ButtonStyle::Subtle)
519 .label_size(LabelSize::Small)
520 .tooltip(move |window, cx| {
521 Tooltip::for_action(
522 "Recent Projects",
523 &zed_actions::OpenRecent {
524 create_new_window: false,
525 },
526 window,
527 cx,
528 )
529 })
530 .on_click(cx.listener(move |_, _, window, cx| {
531 window.dispatch_action(
532 OpenRecent {
533 create_new_window: false,
534 }
535 .boxed_clone(),
536 cx,
537 );
538 }))
539 }
540
541 pub fn render_project_branch(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
542 let repository = self.project.read(cx).active_repository(cx)?;
543 let workspace = self.workspace.upgrade()?;
544 let branch_name = {
545 let repo = repository.read(cx);
546 repo.branch
547 .as_ref()
548 .map(|branch| branch.name())
549 .map(|name| util::truncate_and_trailoff(&name, MAX_BRANCH_NAME_LENGTH))
550 .or_else(|| {
551 repo.head_commit.as_ref().map(|commit| {
552 commit
553 .sha
554 .chars()
555 .take(MAX_SHORT_SHA_LENGTH)
556 .collect::<String>()
557 })
558 })
559 }?;
560
561 Some(
562 Button::new("project_branch_trigger", branch_name)
563 .color(Color::Muted)
564 .style(ButtonStyle::Subtle)
565 .label_size(LabelSize::Small)
566 .tooltip(move |window, cx| {
567 Tooltip::with_meta(
568 "Recent Branches",
569 Some(&zed_actions::git::Branch),
570 "Local branches only",
571 window,
572 cx,
573 )
574 })
575 .on_click(move |_, window, cx| {
576 let _ = workspace.update(cx, |_this, cx| {
577 window.dispatch_action(zed_actions::git::Branch.boxed_clone(), cx);
578 });
579 })
580 .when(
581 TitleBarSettings::get_global(cx).show_branch_icon,
582 |branch_button| {
583 branch_button
584 .icon(IconName::GitBranch)
585 .icon_position(IconPosition::Start)
586 .icon_color(Color::Muted)
587 },
588 ),
589 )
590 }
591
592 fn window_activation_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
593 if window.is_window_active() {
594 ActiveCall::global(cx)
595 .update(cx, |call, cx| call.set_location(Some(&self.project), cx))
596 .detach_and_log_err(cx);
597 } else if cx.active_window().is_none() {
598 ActiveCall::global(cx)
599 .update(cx, |call, cx| call.set_location(None, cx))
600 .detach_and_log_err(cx);
601 }
602 self.workspace
603 .update(cx, |workspace, cx| {
604 workspace.update_active_view_for_followers(window, cx);
605 })
606 .ok();
607 }
608
609 fn active_call_changed(&mut self, cx: &mut Context<Self>) {
610 cx.notify();
611 }
612
613 fn share_project(&mut self, cx: &mut Context<Self>) {
614 let active_call = ActiveCall::global(cx);
615 let project = self.project.clone();
616 active_call
617 .update(cx, |call, cx| call.share_project(project, cx))
618 .detach_and_log_err(cx);
619 }
620
621 fn unshare_project(&mut self, _: &mut Window, cx: &mut Context<Self>) {
622 let active_call = ActiveCall::global(cx);
623 let project = self.project.clone();
624 active_call
625 .update(cx, |call, cx| call.unshare_project(project, cx))
626 .log_err();
627 }
628
629 fn render_connection_status(
630 &self,
631 status: &client::Status,
632 cx: &mut Context<Self>,
633 ) -> Option<AnyElement> {
634 match status {
635 client::Status::ConnectionError
636 | client::Status::ConnectionLost
637 | client::Status::Reauthenticating { .. }
638 | client::Status::Reconnecting { .. }
639 | client::Status::ReconnectionError { .. } => Some(
640 div()
641 .id("disconnected")
642 .child(Icon::new(IconName::Disconnected).size(IconSize::Small))
643 .tooltip(Tooltip::text("Disconnected"))
644 .into_any_element(),
645 ),
646 client::Status::UpgradeRequired => {
647 let auto_updater = auto_update::AutoUpdater::get(cx);
648 let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) {
649 Some(AutoUpdateStatus::Updated { .. }) => "Please restart Zed to Collaborate",
650 Some(AutoUpdateStatus::Installing { .. })
651 | Some(AutoUpdateStatus::Downloading { .. })
652 | Some(AutoUpdateStatus::Checking) => "Updating...",
653 Some(AutoUpdateStatus::Idle) | Some(AutoUpdateStatus::Errored) | None => {
654 "Please update Zed to Collaborate"
655 }
656 };
657
658 Some(
659 Button::new("connection-status", label)
660 .label_size(LabelSize::Small)
661 .on_click(|_, window, cx| {
662 if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) {
663 if auto_updater.read(cx).status().is_updated() {
664 workspace::reload(&Default::default(), cx);
665 return;
666 }
667 }
668 auto_update::check(&Default::default(), window, cx);
669 })
670 .into_any_element(),
671 )
672 }
673 _ => None,
674 }
675 }
676
677 pub fn render_sign_in_button(&mut self, _: &mut Context<Self>) -> Button {
678 let client = self.client.clone();
679 Button::new("sign_in", "Sign in")
680 .label_size(LabelSize::Small)
681 .on_click(move |_, window, cx| {
682 let client = client.clone();
683 window
684 .spawn(cx, async move |cx| {
685 client
686 .authenticate_and_connect(true, &cx)
687 .await
688 .into_response()
689 .notify_async_err(cx);
690 })
691 .detach();
692 })
693 }
694
695 pub fn render_user_menu_button(&mut self, cx: &mut Context<Self>) -> impl Element {
696 let user_store = self.user_store.read(cx);
697 if let Some(user) = user_store.current_user() {
698 let has_subscription_period = self.user_store.read(cx).subscription_period().is_some();
699 let plan = self.user_store.read(cx).current_plan().filter(|_| {
700 // Since the user might be on the legacy free plan we filter based on whether we have a subscription period.
701 has_subscription_period
702 });
703 PopoverMenu::new("user-menu")
704 .anchor(Corner::TopRight)
705 .menu(move |window, cx| {
706 ContextMenu::build(window, cx, |menu, _, _cx| {
707 menu.link(
708 format!(
709 "Current Plan: {}",
710 match plan {
711 None => "None",
712 Some(proto::Plan::Free) => "Zed Free",
713 Some(proto::Plan::ZedPro) => "Zed Pro",
714 Some(proto::Plan::ZedProTrial) => "Zed Pro (Trial)",
715 }
716 ),
717 zed_actions::OpenAccountSettings.boxed_clone(),
718 )
719 .separator()
720 .action("Settings", zed_actions::OpenSettings.boxed_clone())
721 .action("Key Bindings", Box::new(zed_actions::OpenKeymap))
722 .action(
723 "Themes…",
724 zed_actions::theme_selector::Toggle::default().boxed_clone(),
725 )
726 .action(
727 "Icon Themes…",
728 zed_actions::icon_theme_selector::Toggle::default().boxed_clone(),
729 )
730 .action(
731 "Extensions",
732 zed_actions::Extensions::default().boxed_clone(),
733 )
734 .separator()
735 .action("Sign Out", client::SignOut.boxed_clone())
736 })
737 .into()
738 })
739 .trigger_with_tooltip(
740 ButtonLike::new("user-menu")
741 .child(
742 h_flex()
743 .gap_0p5()
744 .children(
745 TitleBarSettings::get_global(cx)
746 .show_user_picture
747 .then(|| Avatar::new(user.avatar_uri.clone())),
748 )
749 .child(
750 Icon::new(IconName::ChevronDown)
751 .size(IconSize::Small)
752 .color(Color::Muted),
753 ),
754 )
755 .style(ButtonStyle::Subtle),
756 Tooltip::text("Toggle User Menu"),
757 )
758 .anchor(gpui::Corner::TopRight)
759 } else {
760 PopoverMenu::new("user-menu")
761 .anchor(Corner::TopRight)
762 .menu(|window, cx| {
763 ContextMenu::build(window, cx, |menu, _, _| {
764 menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
765 .action("Key Bindings", Box::new(zed_actions::OpenKeymap))
766 .action(
767 "Themes…",
768 zed_actions::theme_selector::Toggle::default().boxed_clone(),
769 )
770 .action(
771 "Icon Themes…",
772 zed_actions::icon_theme_selector::Toggle::default().boxed_clone(),
773 )
774 .action(
775 "Extensions",
776 zed_actions::Extensions::default().boxed_clone(),
777 )
778 })
779 .into()
780 })
781 .trigger_with_tooltip(
782 IconButton::new("user-menu", IconName::ChevronDown).icon_size(IconSize::Small),
783 Tooltip::text("Toggle User Menu"),
784 )
785 }
786 }
787}
788
789impl InteractiveElement for TitleBar {
790 fn interactivity(&mut self) -> &mut Interactivity {
791 self.content.interactivity()
792 }
793}
794
795impl StatefulInteractiveElement for TitleBar {}
796
797impl ParentElement for TitleBar {
798 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
799 self.children.extend(elements)
800 }
801}