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