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::{OpenBrowser, 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
52const BOOK_ONBOARDING: &str = "https://dub.sh/zed-c-onboarding";
53
54actions!(collab, [ToggleUserMenu, ToggleProjectMenu, SwitchBranch]);
55
56pub fn init(cx: &mut App) {
57 TitleBarSettings::register(cx);
58
59 cx.observe_new(|workspace: &mut Workspace, window, cx| {
60 let Some(window) = window else {
61 return;
62 };
63 let item = cx.new(|cx| TitleBar::new("title-bar", workspace, window, cx));
64 workspace.set_titlebar_item(item.into(), window, cx);
65
66 #[cfg(not(target_os = "macos"))]
67 workspace.register_action(|workspace, action: &OpenApplicationMenu, window, cx| {
68 if let Some(titlebar) = workspace
69 .titlebar_item()
70 .and_then(|item| item.downcast::<TitleBar>().ok())
71 {
72 titlebar.update(cx, |titlebar, cx| {
73 if let Some(ref menu) = titlebar.application_menu {
74 menu.update(cx, |menu, cx| menu.open_menu(action, window, cx));
75 }
76 });
77 }
78 });
79
80 #[cfg(not(target_os = "macos"))]
81 workspace.register_action(|workspace, _: &ActivateMenuRight, window, cx| {
82 if let Some(titlebar) = workspace
83 .titlebar_item()
84 .and_then(|item| item.downcast::<TitleBar>().ok())
85 {
86 titlebar.update(cx, |titlebar, cx| {
87 if let Some(ref menu) = titlebar.application_menu {
88 menu.update(cx, |menu, cx| {
89 menu.navigate_menus_in_direction(ActivateDirection::Right, window, cx)
90 });
91 }
92 });
93 }
94 });
95
96 #[cfg(not(target_os = "macos"))]
97 workspace.register_action(|workspace, _: &ActivateMenuLeft, window, cx| {
98 if let Some(titlebar) = workspace
99 .titlebar_item()
100 .and_then(|item| item.downcast::<TitleBar>().ok())
101 {
102 titlebar.update(cx, |titlebar, cx| {
103 if let Some(ref menu) = titlebar.application_menu {
104 menu.update(cx, |menu, cx| {
105 menu.navigate_menus_in_direction(ActivateDirection::Left, window, cx)
106 });
107 }
108 });
109 }
110 });
111 })
112 .detach();
113}
114
115pub struct TitleBar {
116 platform_style: PlatformStyle,
117 content: Stateful<Div>,
118 children: SmallVec<[AnyElement; 2]>,
119 project: Entity<Project>,
120 user_store: Entity<UserStore>,
121 client: Arc<Client>,
122 workspace: WeakEntity<Workspace>,
123 should_move: bool,
124 application_menu: Option<Entity<ApplicationMenu>>,
125 _subscriptions: Vec<Subscription>,
126 banner: Entity<OnboardingBanner>,
127}
128
129impl Render for TitleBar {
130 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
131 let title_bar_settings = *TitleBarSettings::get_global(cx);
132 let close_action = Box::new(workspace::CloseWindow);
133 let height = Self::height(window);
134 let supported_controls = window.window_controls();
135 let decorations = window.window_decorations();
136 let titlebar_color = if cfg!(any(target_os = "linux", target_os = "freebsd")) {
137 if window.is_window_active() && !self.should_move {
138 cx.theme().colors().title_bar_background
139 } else {
140 cx.theme().colors().title_bar_inactive_background
141 }
142 } else {
143 cx.theme().colors().title_bar_background
144 };
145
146 h_flex()
147 .id("titlebar")
148 .w_full()
149 .h(height)
150 .map(|this| {
151 if window.is_fullscreen() {
152 this.pl_2()
153 } else if self.platform_style == PlatformStyle::Mac {
154 this.pl(px(platform_mac::TRAFFIC_LIGHT_PADDING))
155 } else {
156 this.pl_2()
157 }
158 })
159 .map(|el| match decorations {
160 Decorations::Server => el,
161 Decorations::Client { tiling, .. } => el
162 .when(!(tiling.top || tiling.right), |el| {
163 el.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
164 })
165 .when(!(tiling.top || tiling.left), |el| {
166 el.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
167 })
168 // this border is to avoid a transparent gap in the rounded corners
169 .mt(px(-1.))
170 .border(px(1.))
171 .border_color(titlebar_color),
172 })
173 .bg(titlebar_color)
174 .content_stretch()
175 .child(
176 div()
177 .id("titlebar-content")
178 .flex()
179 .flex_row()
180 .items_center()
181 .justify_between()
182 .w_full()
183 // Note: On Windows the title bar behavior is handled by the platform implementation.
184 .when(self.platform_style != PlatformStyle::Windows, |this| {
185 this.on_click(|event, window, _| {
186 if event.up.click_count == 2 {
187 window.zoom_window();
188 }
189 })
190 })
191 .child(
192 h_flex()
193 .gap_1()
194 .map(|title_bar| {
195 let mut render_project_items = title_bar_settings.show_branch_name
196 || title_bar_settings.show_project_items;
197 title_bar
198 .when_some(self.application_menu.clone(), |title_bar, menu| {
199 render_project_items &= !menu.read(cx).all_menus_shown();
200 title_bar.child(menu)
201 })
202 .when(render_project_items, |title_bar| {
203 title_bar
204 .when(
205 title_bar_settings.show_project_items,
206 |title_bar| {
207 title_bar
208 .children(self.render_project_host(cx))
209 .child(self.render_project_name(cx))
210 },
211 )
212 .when(
213 title_bar_settings.show_branch_name,
214 |title_bar| {
215 title_bar
216 .children(self.render_project_branch(cx))
217 },
218 )
219 })
220 })
221 .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()),
222 )
223 .child(self.render_collaborator_list(window, cx))
224 .when(title_bar_settings.show_onboarding_banner, |title_bar| {
225 title_bar.child(self.banner.clone())
226 })
227 .child(
228 h_flex()
229 .gap_1()
230 .pr_1()
231 .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
232 .children(self.render_call_controls(window, cx))
233 .map(|el| {
234 let status = self.client.status();
235 let status = &*status.borrow();
236 if matches!(status, client::Status::Connected { .. }) {
237 el.child(self.render_user_menu_button(cx))
238 } else {
239 el.children(self.render_connection_status(status, cx))
240 .child(self.render_sign_in_button(cx))
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 meta.clone(),
435 window,
436 cx,
437 )
438 })
439 .on_click(|_, window, cx| {
440 window.dispatch_action(OpenRemote.boxed_clone(), cx);
441 })
442 .into_any_element(),
443 )
444 }
445
446 pub fn render_project_host(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
447 if self.project.read(cx).is_via_ssh() {
448 return self.render_ssh_project_host(cx);
449 }
450
451 if self.project.read(cx).is_disconnected(cx) {
452 return Some(
453 Button::new("disconnected", "Disconnected")
454 .disabled(true)
455 .color(Color::Disabled)
456 .style(ButtonStyle::Subtle)
457 .label_size(LabelSize::Small)
458 .into_any_element(),
459 );
460 }
461
462 let host = self.project.read(cx).host()?;
463 let host_user = self.user_store.read(cx).get_cached_user(host.user_id)?;
464 let participant_index = self
465 .user_store
466 .read(cx)
467 .participant_indices()
468 .get(&host_user.id)?;
469 Some(
470 Button::new("project_owner_trigger", host_user.github_login.clone())
471 .color(Color::Player(participant_index.0))
472 .style(ButtonStyle::Subtle)
473 .label_size(LabelSize::Small)
474 .tooltip(Tooltip::text(format!(
475 "{} is sharing this project. Click to follow.",
476 host_user.github_login.clone()
477 )))
478 .on_click({
479 let host_peer_id = host.peer_id;
480 cx.listener(move |this, _, window, cx| {
481 this.workspace
482 .update(cx, |workspace, cx| {
483 workspace.follow(host_peer_id, window, cx);
484 })
485 .log_err();
486 })
487 })
488 .into_any_element(),
489 )
490 }
491
492 pub fn render_project_name(&self, cx: &mut Context<Self>) -> impl IntoElement {
493 let name = {
494 let mut names = self.project.read(cx).visible_worktrees(cx).map(|worktree| {
495 let worktree = worktree.read(cx);
496 worktree.root_name()
497 });
498
499 names.next()
500 };
501 let is_project_selected = name.is_some();
502 let name = if let Some(name) = name {
503 util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH)
504 } else {
505 "Open recent project".to_string()
506 };
507
508 Button::new("project_name_trigger", name)
509 .when(!is_project_selected, |b| b.color(Color::Muted))
510 .style(ButtonStyle::Subtle)
511 .label_size(LabelSize::Small)
512 .tooltip(move |window, cx| {
513 Tooltip::for_action(
514 "Recent Projects",
515 &zed_actions::OpenRecent {
516 create_new_window: false,
517 },
518 window,
519 cx,
520 )
521 })
522 .on_click(cx.listener(move |_, _, window, cx| {
523 window.dispatch_action(
524 OpenRecent {
525 create_new_window: false,
526 }
527 .boxed_clone(),
528 cx,
529 );
530 }))
531 }
532
533 pub fn render_project_branch(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
534 let repository = self.project.read(cx).active_repository(cx)?;
535 let workspace = self.workspace.upgrade()?;
536 let branch_name = {
537 let repo = repository.read(cx);
538 repo.branch
539 .as_ref()
540 .map(|branch| branch.name())
541 .map(|name| util::truncate_and_trailoff(&name, MAX_BRANCH_NAME_LENGTH))
542 .or_else(|| {
543 repo.head_commit.as_ref().map(|commit| {
544 commit
545 .sha
546 .chars()
547 .take(MAX_SHORT_SHA_LENGTH)
548 .collect::<String>()
549 })
550 })
551 }?;
552
553 Some(
554 Button::new("project_branch_trigger", branch_name)
555 .color(Color::Muted)
556 .style(ButtonStyle::Subtle)
557 .label_size(LabelSize::Small)
558 .tooltip(move |window, cx| {
559 Tooltip::with_meta(
560 "Recent Branches",
561 Some(&zed_actions::git::Branch),
562 "Local branches only",
563 window,
564 cx,
565 )
566 })
567 .on_click(move |_, window, cx| {
568 let _ = workspace.update(cx, |_this, cx| {
569 window.dispatch_action(zed_actions::git::Branch.boxed_clone(), cx);
570 });
571 })
572 .when(
573 TitleBarSettings::get_global(cx).show_branch_icon,
574 |branch_button| {
575 branch_button
576 .icon(IconName::GitBranch)
577 .icon_position(IconPosition::Start)
578 .icon_color(Color::Muted)
579 },
580 ),
581 )
582 }
583
584 fn window_activation_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
585 if window.is_window_active() {
586 ActiveCall::global(cx)
587 .update(cx, |call, cx| call.set_location(Some(&self.project), cx))
588 .detach_and_log_err(cx);
589 } else if cx.active_window().is_none() {
590 ActiveCall::global(cx)
591 .update(cx, |call, cx| call.set_location(None, cx))
592 .detach_and_log_err(cx);
593 }
594 self.workspace
595 .update(cx, |workspace, cx| {
596 workspace.update_active_view_for_followers(window, cx);
597 })
598 .ok();
599 }
600
601 fn active_call_changed(&mut self, cx: &mut Context<Self>) {
602 cx.notify();
603 }
604
605 fn share_project(&mut self, cx: &mut Context<Self>) {
606 let active_call = ActiveCall::global(cx);
607 let project = self.project.clone();
608 active_call
609 .update(cx, |call, cx| call.share_project(project, cx))
610 .detach_and_log_err(cx);
611 }
612
613 fn unshare_project(&mut self, _: &mut Window, 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.unshare_project(project, cx))
618 .log_err();
619 }
620
621 fn render_connection_status(
622 &self,
623 status: &client::Status,
624 cx: &mut Context<Self>,
625 ) -> Option<AnyElement> {
626 match status {
627 client::Status::ConnectionError
628 | client::Status::ConnectionLost
629 | client::Status::Reauthenticating { .. }
630 | client::Status::Reconnecting { .. }
631 | client::Status::ReconnectionError { .. } => Some(
632 div()
633 .id("disconnected")
634 .child(Icon::new(IconName::Disconnected).size(IconSize::Small))
635 .tooltip(Tooltip::text("Disconnected"))
636 .into_any_element(),
637 ),
638 client::Status::UpgradeRequired => {
639 let auto_updater = auto_update::AutoUpdater::get(cx);
640 let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) {
641 Some(AutoUpdateStatus::Updated { .. }) => "Please restart Zed to Collaborate",
642 Some(AutoUpdateStatus::Installing)
643 | Some(AutoUpdateStatus::Downloading)
644 | Some(AutoUpdateStatus::Checking) => "Updating...",
645 Some(AutoUpdateStatus::Idle) | Some(AutoUpdateStatus::Errored) | None => {
646 "Please update Zed to Collaborate"
647 }
648 };
649
650 Some(
651 Button::new("connection-status", label)
652 .label_size(LabelSize::Small)
653 .on_click(|_, window, cx| {
654 if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) {
655 if auto_updater.read(cx).status().is_updated() {
656 workspace::reload(&Default::default(), cx);
657 return;
658 }
659 }
660 auto_update::check(&Default::default(), window, cx);
661 })
662 .into_any_element(),
663 )
664 }
665 _ => None,
666 }
667 }
668
669 pub fn render_sign_in_button(&mut self, _: &mut Context<Self>) -> Button {
670 let client = self.client.clone();
671 Button::new("sign_in", "Sign in")
672 .label_size(LabelSize::Small)
673 .on_click(move |_, window, cx| {
674 let client = client.clone();
675 window
676 .spawn(cx, async move |cx| {
677 client
678 .authenticate_and_connect(true, &cx)
679 .await
680 .into_response()
681 .notify_async_err(cx);
682 })
683 .detach();
684 })
685 }
686
687 pub fn render_user_menu_button(&mut self, cx: &mut Context<Self>) -> impl Element {
688 let user_store = self.user_store.read(cx);
689 if let Some(user) = user_store.current_user() {
690 let has_subscription_period = self.user_store.read(cx).subscription_period().is_some();
691 let plan = self.user_store.read(cx).current_plan().filter(|_| {
692 // Since the user might be on the legacy free plan we filter based on whether we have a subscription period.
693 has_subscription_period
694 });
695 PopoverMenu::new("user-menu")
696 .anchor(Corner::TopRight)
697 .menu(move |window, cx| {
698 ContextMenu::build(window, cx, |menu, _, _cx| {
699 menu.link(
700 format!(
701 "Current Plan: {}",
702 match plan {
703 None => "None",
704 Some(proto::Plan::Free) => "Zed Free",
705 Some(proto::Plan::ZedPro) => "Zed Pro",
706 Some(proto::Plan::ZedProTrial) => "Zed Pro (Trial)",
707 }
708 ),
709 zed_actions::OpenAccountSettings.boxed_clone(),
710 )
711 .separator()
712 .action("Settings", zed_actions::OpenSettings.boxed_clone())
713 .action("Key Bindings", Box::new(zed_actions::OpenKeymap))
714 .action(
715 "Themes…",
716 zed_actions::theme_selector::Toggle::default().boxed_clone(),
717 )
718 .action(
719 "Icon Themes…",
720 zed_actions::icon_theme_selector::Toggle::default().boxed_clone(),
721 )
722 .action(
723 "Extensions",
724 zed_actions::Extensions::default().boxed_clone(),
725 )
726 .separator()
727 .link(
728 "Book Onboarding",
729 OpenBrowser {
730 url: BOOK_ONBOARDING.to_string(),
731 }
732 .boxed_clone(),
733 )
734 .action("Sign Out", client::SignOut.boxed_clone())
735 })
736 .into()
737 })
738 .trigger_with_tooltip(
739 ButtonLike::new("user-menu")
740 .child(
741 h_flex()
742 .gap_0p5()
743 .children(
744 TitleBarSettings::get_global(cx)
745 .show_user_picture
746 .then(|| Avatar::new(user.avatar_uri.clone())),
747 )
748 .child(
749 Icon::new(IconName::ChevronDown)
750 .size(IconSize::Small)
751 .color(Color::Muted),
752 ),
753 )
754 .style(ButtonStyle::Subtle),
755 Tooltip::text("Toggle User Menu"),
756 )
757 .anchor(gpui::Corner::TopRight)
758 } else {
759 PopoverMenu::new("user-menu")
760 .anchor(Corner::TopRight)
761 .menu(|window, cx| {
762 ContextMenu::build(window, cx, |menu, _, _| {
763 menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
764 .action("Key Bindings", Box::new(zed_actions::OpenKeymap))
765 .action(
766 "Themes…",
767 zed_actions::theme_selector::Toggle::default().boxed_clone(),
768 )
769 .action(
770 "Icon Themes…",
771 zed_actions::icon_theme_selector::Toggle::default().boxed_clone(),
772 )
773 .action(
774 "Extensions",
775 zed_actions::Extensions::default().boxed_clone(),
776 )
777 .separator()
778 .link(
779 "Book Onboarding",
780 OpenBrowser {
781 url: BOOK_ONBOARDING.to_string(),
782 }
783 .boxed_clone(),
784 )
785 })
786 .into()
787 })
788 .trigger_with_tooltip(
789 IconButton::new("user-menu", IconName::ChevronDown).icon_size(IconSize::Small),
790 Tooltip::text("Toggle User Menu"),
791 )
792 }
793 }
794}
795
796impl InteractiveElement for TitleBar {
797 fn interactivity(&mut self) -> &mut Interactivity {
798 self.content.interactivity()
799 }
800}
801
802impl StatefulInteractiveElement for TitleBar {}
803
804impl ParentElement for TitleBar {
805 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
806 self.children.extend(elements)
807 }
808}