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 .when(TitleBarSettings::get_global(cx).show_sign_in, |el| {
241 el.child(self.render_sign_in_button(cx))
242 })
243 .child(self.render_user_menu_button(cx))
244 }
245 }),
246 ),
247 )
248 .when(!window.is_fullscreen(), |title_bar| {
249 match self.platform_style {
250 PlatformStyle::Mac => title_bar,
251 PlatformStyle::Linux => {
252 if matches!(decorations, Decorations::Client { .. }) {
253 title_bar
254 .child(platform_linux::LinuxWindowControls::new(close_action))
255 .when(supported_controls.window_menu, |titlebar| {
256 titlebar.on_mouse_down(
257 gpui::MouseButton::Right,
258 move |ev, window, _| window.show_window_menu(ev.position),
259 )
260 })
261 .on_mouse_move(cx.listener(move |this, _ev, window, _| {
262 if this.should_move {
263 this.should_move = false;
264 window.start_window_move();
265 }
266 }))
267 .on_mouse_down_out(cx.listener(move |this, _ev, _window, _cx| {
268 this.should_move = false;
269 }))
270 .on_mouse_up(
271 gpui::MouseButton::Left,
272 cx.listener(move |this, _ev, _window, _cx| {
273 this.should_move = false;
274 }),
275 )
276 .on_mouse_down(
277 gpui::MouseButton::Left,
278 cx.listener(move |this, _ev, _window, _cx| {
279 this.should_move = true;
280 }),
281 )
282 } else {
283 title_bar
284 }
285 }
286 PlatformStyle::Windows => {
287 title_bar.child(platform_windows::WindowsWindowControls::new(height))
288 }
289 }
290 })
291 }
292}
293
294impl TitleBar {
295 pub fn new(
296 id: impl Into<ElementId>,
297 workspace: &Workspace,
298 window: &mut Window,
299 cx: &mut Context<Self>,
300 ) -> Self {
301 let project = workspace.project().clone();
302 let user_store = workspace.app_state().user_store.clone();
303 let client = workspace.app_state().client.clone();
304 let active_call = ActiveCall::global(cx);
305
306 let platform_style = PlatformStyle::platform();
307 let application_menu = match platform_style {
308 PlatformStyle::Mac => {
309 if option_env!("ZED_USE_CROSS_PLATFORM_MENU").is_some() {
310 Some(cx.new(|cx| ApplicationMenu::new(window, cx)))
311 } else {
312 None
313 }
314 }
315 PlatformStyle::Linux | PlatformStyle::Windows => {
316 Some(cx.new(|cx| ApplicationMenu::new(window, cx)))
317 }
318 };
319
320 let mut subscriptions = Vec::new();
321 subscriptions.push(
322 cx.observe(&workspace.weak_handle().upgrade().unwrap(), |_, _, cx| {
323 cx.notify()
324 }),
325 );
326 subscriptions.push(cx.subscribe(&project, |_, _, _: &project::Event, cx| cx.notify()));
327 subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx)));
328 subscriptions.push(cx.observe_window_activation(window, Self::window_activation_changed));
329 subscriptions.push(cx.observe(&user_store, |_, _, cx| cx.notify()));
330
331 let banner = cx.new(|cx| {
332 OnboardingBanner::new(
333 "Agentic Onboarding",
334 IconName::ZedAssistant,
335 "Agentic Editing",
336 None,
337 zed_actions::agent::OpenOnboardingModal.boxed_clone(),
338 cx,
339 )
340 });
341
342 Self {
343 platform_style,
344 content: div().id(id.into()),
345 children: SmallVec::new(),
346 application_menu,
347 workspace: workspace.weak_handle(),
348 should_move: false,
349 project,
350 user_store,
351 client,
352 _subscriptions: subscriptions,
353 banner,
354 }
355 }
356
357 #[cfg(not(target_os = "windows"))]
358 pub fn height(window: &mut Window) -> Pixels {
359 (1.75 * window.rem_size()).max(px(34.))
360 }
361
362 #[cfg(target_os = "windows")]
363 pub fn height(_window: &mut Window) -> Pixels {
364 // todo(windows) instead of hard coded size report the actual size to the Windows platform API
365 px(32.)
366 }
367
368 /// Sets the platform style.
369 pub fn platform_style(mut self, style: PlatformStyle) -> Self {
370 self.platform_style = style;
371 self
372 }
373
374 fn render_ssh_project_host(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
375 let options = self.project.read(cx).ssh_connection_options(cx)?;
376 let host: SharedString = options.connection_string().into();
377
378 let nickname = options
379 .nickname
380 .clone()
381 .map(|nick| nick.into())
382 .unwrap_or_else(|| host.clone());
383
384 let (indicator_color, meta) = match self.project.read(cx).ssh_connection_state(cx)? {
385 remote::ConnectionState::Connecting => (Color::Info, format!("Connecting to: {host}")),
386 remote::ConnectionState::Connected => (Color::Success, format!("Connected to: {host}")),
387 remote::ConnectionState::HeartbeatMissed => (
388 Color::Warning,
389 format!("Connection attempt to {host} missed. Retrying..."),
390 ),
391 remote::ConnectionState::Reconnecting => (
392 Color::Warning,
393 format!("Lost connection to {host}. Reconnecting..."),
394 ),
395 remote::ConnectionState::Disconnected => {
396 (Color::Error, format!("Disconnected from {host}"))
397 }
398 };
399
400 let icon_color = match self.project.read(cx).ssh_connection_state(cx)? {
401 remote::ConnectionState::Connecting => Color::Info,
402 remote::ConnectionState::Connected => Color::Default,
403 remote::ConnectionState::HeartbeatMissed => Color::Warning,
404 remote::ConnectionState::Reconnecting => Color::Warning,
405 remote::ConnectionState::Disconnected => Color::Error,
406 };
407
408 let meta = SharedString::from(meta);
409
410 Some(
411 ButtonLike::new("ssh-server-icon")
412 .child(
413 h_flex()
414 .gap_2()
415 .max_w_32()
416 .child(
417 IconWithIndicator::new(
418 Icon::new(IconName::Server)
419 .size(IconSize::XSmall)
420 .color(icon_color),
421 Some(Indicator::dot().color(indicator_color)),
422 )
423 .indicator_border_color(Some(cx.theme().colors().title_bar_background))
424 .into_any_element(),
425 )
426 .child(
427 Label::new(nickname.clone())
428 .size(LabelSize::Small)
429 .truncate(),
430 ),
431 )
432 .tooltip(move |window, cx| {
433 Tooltip::with_meta(
434 "Remote Project",
435 Some(&OpenRemote),
436 meta.clone(),
437 window,
438 cx,
439 )
440 })
441 .on_click(|_, window, cx| {
442 window.dispatch_action(OpenRemote.boxed_clone(), cx);
443 })
444 .into_any_element(),
445 )
446 }
447
448 pub fn render_project_host(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
449 if self.project.read(cx).is_via_ssh() {
450 return self.render_ssh_project_host(cx);
451 }
452
453 if self.project.read(cx).is_disconnected(cx) {
454 return Some(
455 Button::new("disconnected", "Disconnected")
456 .disabled(true)
457 .color(Color::Disabled)
458 .style(ButtonStyle::Subtle)
459 .label_size(LabelSize::Small)
460 .into_any_element(),
461 );
462 }
463
464 let host = self.project.read(cx).host()?;
465 let host_user = self.user_store.read(cx).get_cached_user(host.user_id)?;
466 let participant_index = self
467 .user_store
468 .read(cx)
469 .participant_indices()
470 .get(&host_user.id)?;
471 Some(
472 Button::new("project_owner_trigger", host_user.github_login.clone())
473 .color(Color::Player(participant_index.0))
474 .style(ButtonStyle::Subtle)
475 .label_size(LabelSize::Small)
476 .tooltip(Tooltip::text(format!(
477 "{} is sharing this project. Click to follow.",
478 host_user.github_login
479 )))
480 .on_click({
481 let host_peer_id = host.peer_id;
482 cx.listener(move |this, _, window, cx| {
483 this.workspace
484 .update(cx, |workspace, cx| {
485 workspace.follow(host_peer_id, window, cx);
486 })
487 .log_err();
488 })
489 })
490 .into_any_element(),
491 )
492 }
493
494 pub fn render_project_name(&self, cx: &mut Context<Self>) -> impl IntoElement {
495 let name = {
496 let mut names = self.project.read(cx).visible_worktrees(cx).map(|worktree| {
497 let worktree = worktree.read(cx);
498 worktree.root_name()
499 });
500
501 names.next()
502 };
503 let is_project_selected = name.is_some();
504 let name = if let Some(name) = name {
505 util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH)
506 } else {
507 "Open recent project".to_string()
508 };
509
510 Button::new("project_name_trigger", name)
511 .when(!is_project_selected, |b| b.color(Color::Muted))
512 .style(ButtonStyle::Subtle)
513 .label_size(LabelSize::Small)
514 .tooltip(move |window, cx| {
515 Tooltip::for_action(
516 "Recent Projects",
517 &zed_actions::OpenRecent {
518 create_new_window: false,
519 },
520 window,
521 cx,
522 )
523 })
524 .on_click(cx.listener(move |_, _, window, cx| {
525 window.dispatch_action(
526 OpenRecent {
527 create_new_window: false,
528 }
529 .boxed_clone(),
530 cx,
531 );
532 }))
533 }
534
535 pub fn render_project_branch(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
536 let repository = self.project.read(cx).active_repository(cx)?;
537 let workspace = self.workspace.upgrade()?;
538 let branch_name = {
539 let repo = repository.read(cx);
540 repo.branch
541 .as_ref()
542 .map(|branch| branch.name())
543 .map(|name| util::truncate_and_trailoff(&name, MAX_BRANCH_NAME_LENGTH))
544 .or_else(|| {
545 repo.head_commit.as_ref().map(|commit| {
546 commit
547 .sha
548 .chars()
549 .take(MAX_SHORT_SHA_LENGTH)
550 .collect::<String>()
551 })
552 })
553 }?;
554
555 Some(
556 Button::new("project_branch_trigger", branch_name)
557 .color(Color::Muted)
558 .style(ButtonStyle::Subtle)
559 .label_size(LabelSize::Small)
560 .tooltip(move |window, cx| {
561 Tooltip::with_meta(
562 "Recent Branches",
563 Some(&zed_actions::git::Branch),
564 "Local branches only",
565 window,
566 cx,
567 )
568 })
569 .on_click(move |_, window, cx| {
570 let _ = workspace.update(cx, |_this, cx| {
571 window.dispatch_action(zed_actions::git::Branch.boxed_clone(), cx);
572 });
573 })
574 .when(
575 TitleBarSettings::get_global(cx).show_branch_icon,
576 |branch_button| {
577 branch_button
578 .icon(IconName::GitBranch)
579 .icon_position(IconPosition::Start)
580 .icon_color(Color::Muted)
581 },
582 ),
583 )
584 }
585
586 fn window_activation_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
587 if window.is_window_active() {
588 ActiveCall::global(cx)
589 .update(cx, |call, cx| call.set_location(Some(&self.project), cx))
590 .detach_and_log_err(cx);
591 } else if cx.active_window().is_none() {
592 ActiveCall::global(cx)
593 .update(cx, |call, cx| call.set_location(None, cx))
594 .detach_and_log_err(cx);
595 }
596 self.workspace
597 .update(cx, |workspace, cx| {
598 workspace.update_active_view_for_followers(window, cx);
599 })
600 .ok();
601 }
602
603 fn active_call_changed(&mut self, cx: &mut Context<Self>) {
604 cx.notify();
605 }
606
607 fn share_project(&mut self, cx: &mut Context<Self>) {
608 let active_call = ActiveCall::global(cx);
609 let project = self.project.clone();
610 active_call
611 .update(cx, |call, cx| call.share_project(project, cx))
612 .detach_and_log_err(cx);
613 }
614
615 fn unshare_project(&mut self, _: &mut Window, cx: &mut Context<Self>) {
616 let active_call = ActiveCall::global(cx);
617 let project = self.project.clone();
618 active_call
619 .update(cx, |call, cx| call.unshare_project(project, cx))
620 .log_err();
621 }
622
623 fn render_connection_status(
624 &self,
625 status: &client::Status,
626 cx: &mut Context<Self>,
627 ) -> Option<AnyElement> {
628 match status {
629 client::Status::ConnectionError
630 | client::Status::ConnectionLost
631 | client::Status::Reauthenticating { .. }
632 | client::Status::Reconnecting { .. }
633 | client::Status::ReconnectionError { .. } => Some(
634 div()
635 .id("disconnected")
636 .child(Icon::new(IconName::Disconnected).size(IconSize::Small))
637 .tooltip(Tooltip::text("Disconnected"))
638 .into_any_element(),
639 ),
640 client::Status::UpgradeRequired => {
641 let auto_updater = auto_update::AutoUpdater::get(cx);
642 let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) {
643 Some(AutoUpdateStatus::Updated { .. }) => "Please restart Zed to Collaborate",
644 Some(AutoUpdateStatus::Installing)
645 | Some(AutoUpdateStatus::Downloading)
646 | Some(AutoUpdateStatus::Checking) => "Updating...",
647 Some(AutoUpdateStatus::Idle) | Some(AutoUpdateStatus::Errored) | None => {
648 "Please update Zed to Collaborate"
649 }
650 };
651
652 Some(
653 Button::new("connection-status", label)
654 .label_size(LabelSize::Small)
655 .on_click(|_, window, cx| {
656 if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) {
657 if auto_updater.read(cx).status().is_updated() {
658 workspace::reload(&Default::default(), cx);
659 return;
660 }
661 }
662 auto_update::check(&Default::default(), window, cx);
663 })
664 .into_any_element(),
665 )
666 }
667 _ => None,
668 }
669 }
670
671 pub fn render_sign_in_button(&mut self, _: &mut Context<Self>) -> Button {
672 let client = self.client.clone();
673 Button::new("sign_in", "Sign in")
674 .label_size(LabelSize::Small)
675 .on_click(move |_, window, cx| {
676 let client = client.clone();
677 window
678 .spawn(cx, async move |cx| {
679 client
680 .authenticate_and_connect(true, &cx)
681 .await
682 .into_response()
683 .notify_async_err(cx);
684 })
685 .detach();
686 })
687 }
688
689 pub fn render_user_menu_button(&mut self, cx: &mut Context<Self>) -> impl Element {
690 let user_store = self.user_store.read(cx);
691 if let Some(user) = user_store.current_user() {
692 let has_subscription_period = self.user_store.read(cx).subscription_period().is_some();
693 let plan = self.user_store.read(cx).current_plan().filter(|_| {
694 // Since the user might be on the legacy free plan we filter based on whether we have a subscription period.
695 has_subscription_period
696 });
697 PopoverMenu::new("user-menu")
698 .anchor(Corner::TopRight)
699 .menu(move |window, cx| {
700 ContextMenu::build(window, cx, |menu, _, _cx| {
701 menu.link(
702 format!(
703 "Current Plan: {}",
704 match plan {
705 None => "None",
706 Some(proto::Plan::Free) => "Zed Free",
707 Some(proto::Plan::ZedPro) => "Zed Pro",
708 Some(proto::Plan::ZedProTrial) => "Zed Pro (Trial)",
709 }
710 ),
711 zed_actions::OpenAccountSettings.boxed_clone(),
712 )
713 .separator()
714 .action("Settings", zed_actions::OpenSettings.boxed_clone())
715 .action("Key Bindings", Box::new(zed_actions::OpenKeymap))
716 .action(
717 "Themes…",
718 zed_actions::theme_selector::Toggle::default().boxed_clone(),
719 )
720 .action(
721 "Icon Themes…",
722 zed_actions::icon_theme_selector::Toggle::default().boxed_clone(),
723 )
724 .action(
725 "Extensions",
726 zed_actions::Extensions::default().boxed_clone(),
727 )
728 .separator()
729 .link(
730 "Book Onboarding",
731 OpenBrowser {
732 url: BOOK_ONBOARDING.to_string(),
733 }
734 .boxed_clone(),
735 )
736 .action("Sign Out", client::SignOut.boxed_clone())
737 })
738 .into()
739 })
740 .trigger_with_tooltip(
741 ButtonLike::new("user-menu")
742 .child(
743 h_flex()
744 .gap_0p5()
745 .children(
746 TitleBarSettings::get_global(cx)
747 .show_user_picture
748 .then(|| Avatar::new(user.avatar_uri.clone())),
749 )
750 .child(
751 Icon::new(IconName::ChevronDown)
752 .size(IconSize::Small)
753 .color(Color::Muted),
754 ),
755 )
756 .style(ButtonStyle::Subtle),
757 Tooltip::text("Toggle User Menu"),
758 )
759 .anchor(gpui::Corner::TopRight)
760 } else {
761 PopoverMenu::new("user-menu")
762 .anchor(Corner::TopRight)
763 .menu(|window, cx| {
764 ContextMenu::build(window, cx, |menu, _, _| {
765 menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
766 .action("Key Bindings", Box::new(zed_actions::OpenKeymap))
767 .action(
768 "Themes…",
769 zed_actions::theme_selector::Toggle::default().boxed_clone(),
770 )
771 .action(
772 "Icon Themes…",
773 zed_actions::icon_theme_selector::Toggle::default().boxed_clone(),
774 )
775 .action(
776 "Extensions",
777 zed_actions::Extensions::default().boxed_clone(),
778 )
779 .separator()
780 .link(
781 "Book Onboarding",
782 OpenBrowser {
783 url: BOOK_ONBOARDING.to_string(),
784 }
785 .boxed_clone(),
786 )
787 })
788 .into()
789 })
790 .trigger_with_tooltip(
791 IconButton::new("user-menu", IconName::ChevronDown).icon_size(IconSize::Small),
792 Tooltip::text("Toggle User Menu"),
793 )
794 }
795 }
796}
797
798impl InteractiveElement for TitleBar {
799 fn interactivity(&mut self) -> &mut Interactivity {
800 self.content.interactivity()
801 }
802}
803
804impl StatefulInteractiveElement for TitleBar {}
805
806impl ParentElement for TitleBar {
807 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
808 self.children.extend(elements)
809 }
810}