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