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 from_existing_connection: false,
437 }),
438 meta.clone(),
439 window,
440 cx,
441 )
442 })
443 .on_click(|_, window, cx| {
444 window.dispatch_action(
445 OpenRemote {
446 from_existing_connection: false,
447 }
448 .boxed_clone(),
449 cx,
450 );
451 })
452 .into_any_element(),
453 )
454 }
455
456 pub fn render_project_host(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
457 if self.project.read(cx).is_via_ssh() {
458 return self.render_ssh_project_host(cx);
459 }
460
461 if self.project.read(cx).is_disconnected(cx) {
462 return Some(
463 Button::new("disconnected", "Disconnected")
464 .disabled(true)
465 .color(Color::Disabled)
466 .style(ButtonStyle::Subtle)
467 .label_size(LabelSize::Small)
468 .into_any_element(),
469 );
470 }
471
472 let host = self.project.read(cx).host()?;
473 let host_user = self.user_store.read(cx).get_cached_user(host.user_id)?;
474 let participant_index = self
475 .user_store
476 .read(cx)
477 .participant_indices()
478 .get(&host_user.id)?;
479 Some(
480 Button::new("project_owner_trigger", host_user.github_login.clone())
481 .color(Color::Player(participant_index.0))
482 .style(ButtonStyle::Subtle)
483 .label_size(LabelSize::Small)
484 .tooltip(Tooltip::text(format!(
485 "{} is sharing this project. Click to follow.",
486 host_user.github_login
487 )))
488 .on_click({
489 let host_peer_id = host.peer_id;
490 cx.listener(move |this, _, window, cx| {
491 this.workspace
492 .update(cx, |workspace, cx| {
493 workspace.follow(host_peer_id, window, cx);
494 })
495 .log_err();
496 })
497 })
498 .into_any_element(),
499 )
500 }
501
502 pub fn render_project_name(&self, cx: &mut Context<Self>) -> impl IntoElement {
503 let name = {
504 let mut names = self.project.read(cx).visible_worktrees(cx).map(|worktree| {
505 let worktree = worktree.read(cx);
506 worktree.root_name()
507 });
508
509 names.next()
510 };
511 let is_project_selected = name.is_some();
512 let name = if let Some(name) = name {
513 util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH)
514 } else {
515 "Open recent project".to_string()
516 };
517
518 Button::new("project_name_trigger", name)
519 .when(!is_project_selected, |b| b.color(Color::Muted))
520 .style(ButtonStyle::Subtle)
521 .label_size(LabelSize::Small)
522 .tooltip(move |window, cx| {
523 Tooltip::for_action(
524 "Recent Projects",
525 &zed_actions::OpenRecent {
526 create_new_window: false,
527 },
528 window,
529 cx,
530 )
531 })
532 .on_click(cx.listener(move |_, _, window, cx| {
533 window.dispatch_action(
534 OpenRecent {
535 create_new_window: false,
536 }
537 .boxed_clone(),
538 cx,
539 );
540 }))
541 }
542
543 pub fn render_project_branch(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
544 let repository = self.project.read(cx).active_repository(cx)?;
545 let workspace = self.workspace.upgrade()?;
546 let branch_name = {
547 let repo = repository.read(cx);
548 repo.branch
549 .as_ref()
550 .map(|branch| branch.name())
551 .map(|name| util::truncate_and_trailoff(&name, MAX_BRANCH_NAME_LENGTH))
552 .or_else(|| {
553 repo.head_commit.as_ref().map(|commit| {
554 commit
555 .sha
556 .chars()
557 .take(MAX_SHORT_SHA_LENGTH)
558 .collect::<String>()
559 })
560 })
561 }?;
562
563 Some(
564 Button::new("project_branch_trigger", branch_name)
565 .color(Color::Muted)
566 .style(ButtonStyle::Subtle)
567 .label_size(LabelSize::Small)
568 .tooltip(move |window, cx| {
569 Tooltip::with_meta(
570 "Recent Branches",
571 Some(&zed_actions::git::Branch),
572 "Local branches only",
573 window,
574 cx,
575 )
576 })
577 .on_click(move |_, window, cx| {
578 let _ = workspace.update(cx, |_this, cx| {
579 window.dispatch_action(zed_actions::git::Branch.boxed_clone(), cx);
580 });
581 })
582 .when(
583 TitleBarSettings::get_global(cx).show_branch_icon,
584 |branch_button| {
585 branch_button
586 .icon(IconName::GitBranch)
587 .icon_position(IconPosition::Start)
588 .icon_color(Color::Muted)
589 },
590 ),
591 )
592 }
593
594 fn window_activation_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
595 if window.is_window_active() {
596 ActiveCall::global(cx)
597 .update(cx, |call, cx| call.set_location(Some(&self.project), cx))
598 .detach_and_log_err(cx);
599 } else if cx.active_window().is_none() {
600 ActiveCall::global(cx)
601 .update(cx, |call, cx| call.set_location(None, cx))
602 .detach_and_log_err(cx);
603 }
604 self.workspace
605 .update(cx, |workspace, cx| {
606 workspace.update_active_view_for_followers(window, cx);
607 })
608 .ok();
609 }
610
611 fn active_call_changed(&mut self, cx: &mut Context<Self>) {
612 cx.notify();
613 }
614
615 fn share_project(&mut self, 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.share_project(project, cx))
620 .detach_and_log_err(cx);
621 }
622
623 fn unshare_project(&mut self, _: &mut Window, cx: &mut Context<Self>) {
624 let active_call = ActiveCall::global(cx);
625 let project = self.project.clone();
626 active_call
627 .update(cx, |call, cx| call.unshare_project(project, cx))
628 .log_err();
629 }
630
631 fn render_connection_status(
632 &self,
633 status: &client::Status,
634 cx: &mut Context<Self>,
635 ) -> Option<AnyElement> {
636 match status {
637 client::Status::ConnectionError
638 | client::Status::ConnectionLost
639 | client::Status::Reauthenticating { .. }
640 | client::Status::Reconnecting { .. }
641 | client::Status::ReconnectionError { .. } => Some(
642 div()
643 .id("disconnected")
644 .child(Icon::new(IconName::Disconnected).size(IconSize::Small))
645 .tooltip(Tooltip::text("Disconnected"))
646 .into_any_element(),
647 ),
648 client::Status::UpgradeRequired => {
649 let auto_updater = auto_update::AutoUpdater::get(cx);
650 let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) {
651 Some(AutoUpdateStatus::Updated { .. }) => "Please restart Zed to Collaborate",
652 Some(AutoUpdateStatus::Installing)
653 | Some(AutoUpdateStatus::Downloading)
654 | Some(AutoUpdateStatus::Checking) => "Updating...",
655 Some(AutoUpdateStatus::Idle) | Some(AutoUpdateStatus::Errored) | None => {
656 "Please update Zed to Collaborate"
657 }
658 };
659
660 Some(
661 Button::new("connection-status", label)
662 .label_size(LabelSize::Small)
663 .on_click(|_, window, cx| {
664 if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) {
665 if auto_updater.read(cx).status().is_updated() {
666 workspace::reload(&Default::default(), cx);
667 return;
668 }
669 }
670 auto_update::check(&Default::default(), window, cx);
671 })
672 .into_any_element(),
673 )
674 }
675 _ => None,
676 }
677 }
678
679 pub fn render_sign_in_button(&mut self, _: &mut Context<Self>) -> Button {
680 let client = self.client.clone();
681 Button::new("sign_in", "Sign in")
682 .label_size(LabelSize::Small)
683 .on_click(move |_, window, cx| {
684 let client = client.clone();
685 window
686 .spawn(cx, async move |cx| {
687 client
688 .authenticate_and_connect(true, &cx)
689 .await
690 .into_response()
691 .notify_async_err(cx);
692 })
693 .detach();
694 })
695 }
696
697 pub fn render_user_menu_button(&mut self, cx: &mut Context<Self>) -> impl Element {
698 let user_store = self.user_store.read(cx);
699 if let Some(user) = user_store.current_user() {
700 let has_subscription_period = self.user_store.read(cx).subscription_period().is_some();
701 let plan = self.user_store.read(cx).current_plan().filter(|_| {
702 // Since the user might be on the legacy free plan we filter based on whether we have a subscription period.
703 has_subscription_period
704 });
705 PopoverMenu::new("user-menu")
706 .anchor(Corner::TopRight)
707 .menu(move |window, cx| {
708 ContextMenu::build(window, cx, |menu, _, _cx| {
709 menu.link(
710 format!(
711 "Current Plan: {}",
712 match plan {
713 None => "None",
714 Some(proto::Plan::Free) => "Zed Free",
715 Some(proto::Plan::ZedPro) => "Zed Pro",
716 Some(proto::Plan::ZedProTrial) => "Zed Pro (Trial)",
717 }
718 ),
719 zed_actions::OpenAccountSettings.boxed_clone(),
720 )
721 .separator()
722 .action("Settings", zed_actions::OpenSettings.boxed_clone())
723 .action("Key Bindings", Box::new(zed_actions::OpenKeymap))
724 .action(
725 "Themes…",
726 zed_actions::theme_selector::Toggle::default().boxed_clone(),
727 )
728 .action(
729 "Icon Themes…",
730 zed_actions::icon_theme_selector::Toggle::default().boxed_clone(),
731 )
732 .action(
733 "Extensions",
734 zed_actions::Extensions::default().boxed_clone(),
735 )
736 .separator()
737 .link(
738 "Book Onboarding",
739 OpenBrowser {
740 url: BOOK_ONBOARDING.to_string(),
741 }
742 .boxed_clone(),
743 )
744 .action("Sign Out", client::SignOut.boxed_clone())
745 })
746 .into()
747 })
748 .trigger_with_tooltip(
749 ButtonLike::new("user-menu")
750 .child(
751 h_flex()
752 .gap_0p5()
753 .children(
754 TitleBarSettings::get_global(cx)
755 .show_user_picture
756 .then(|| Avatar::new(user.avatar_uri.clone())),
757 )
758 .child(
759 Icon::new(IconName::ChevronDown)
760 .size(IconSize::Small)
761 .color(Color::Muted),
762 ),
763 )
764 .style(ButtonStyle::Subtle),
765 Tooltip::text("Toggle User Menu"),
766 )
767 .anchor(gpui::Corner::TopRight)
768 } else {
769 PopoverMenu::new("user-menu")
770 .anchor(Corner::TopRight)
771 .menu(|window, cx| {
772 ContextMenu::build(window, cx, |menu, _, _| {
773 menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
774 .action("Key Bindings", Box::new(zed_actions::OpenKeymap))
775 .action(
776 "Themes…",
777 zed_actions::theme_selector::Toggle::default().boxed_clone(),
778 )
779 .action(
780 "Icon Themes…",
781 zed_actions::icon_theme_selector::Toggle::default().boxed_clone(),
782 )
783 .action(
784 "Extensions",
785 zed_actions::Extensions::default().boxed_clone(),
786 )
787 .separator()
788 .link(
789 "Book Onboarding",
790 OpenBrowser {
791 url: BOOK_ONBOARDING.to_string(),
792 }
793 .boxed_clone(),
794 )
795 })
796 .into()
797 })
798 .trigger_with_tooltip(
799 IconButton::new("user-menu", IconName::ChevronDown).icon_size(IconSize::Small),
800 Tooltip::text("Toggle User Menu"),
801 )
802 }
803 }
804}
805
806impl InteractiveElement for TitleBar {
807 fn interactivity(&mut self) -> &mut Interactivity {
808 self.content.interactivity()
809 }
810}
811
812impl StatefulInteractiveElement for TitleBar {}
813
814impl ParentElement for TitleBar {
815 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
816 self.children.extend(elements)
817 }
818}