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 feature_flags::{FeatureFlagAppExt, NewBillingFeatureFlag};
23use gpui::{
24 Action, AnyElement, App, Context, Corner, Decorations, Element, Entity, InteractiveElement,
25 Interactivity, IntoElement, MouseButton, ParentElement, Render, Stateful,
26 StatefulInteractiveElement, Styled, Subscription, WeakEntity, Window, actions, div, px,
27};
28use onboarding_banner::OnboardingBanner;
29use project::Project;
30use rpc::proto;
31use settings::Settings as _;
32use smallvec::SmallVec;
33use std::sync::Arc;
34use theme::ActiveTheme;
35use title_bar_settings::TitleBarSettings;
36use ui::{
37 Avatar, Button, ButtonLike, ButtonStyle, ContextMenu, Icon, IconName, IconSize,
38 IconWithIndicator, Indicator, PopoverMenu, Tooltip, h_flex, prelude::*,
39};
40use util::ResultExt;
41use workspace::{Workspace, notifications::NotifyResultExt};
42use zed_actions::{OpenBrowser, OpenRecent, OpenRemote};
43
44pub use onboarding_banner::restore_banner;
45
46#[cfg(feature = "stories")]
47pub use stories::*;
48
49const MAX_PROJECT_NAME_LENGTH: usize = 40;
50const MAX_BRANCH_NAME_LENGTH: usize = 40;
51const MAX_SHORT_SHA_LENGTH: usize = 8;
52
53const BOOK_ONBOARDING: &str = "https://dub.sh/zed-c-onboarding";
54
55actions!(collab, [ToggleUserMenu, ToggleProjectMenu, SwitchBranch]);
56
57pub fn init(cx: &mut App) {
58 TitleBarSettings::register(cx);
59
60 cx.observe_new(|workspace: &mut Workspace, window, cx| {
61 let Some(window) = window else {
62 return;
63 };
64 let item = cx.new(|cx| TitleBar::new("title-bar", workspace, window, cx));
65 workspace.set_titlebar_item(item.into(), window, cx);
66
67 #[cfg(not(target_os = "macos"))]
68 workspace.register_action(|workspace, action: &OpenApplicationMenu, window, cx| {
69 if let Some(titlebar) = workspace
70 .titlebar_item()
71 .and_then(|item| item.downcast::<TitleBar>().ok())
72 {
73 titlebar.update(cx, |titlebar, cx| {
74 if let Some(ref menu) = titlebar.application_menu {
75 menu.update(cx, |menu, cx| menu.open_menu(action, window, cx));
76 }
77 });
78 }
79 });
80
81 #[cfg(not(target_os = "macos"))]
82 workspace.register_action(|workspace, _: &ActivateMenuRight, window, cx| {
83 if let Some(titlebar) = workspace
84 .titlebar_item()
85 .and_then(|item| item.downcast::<TitleBar>().ok())
86 {
87 titlebar.update(cx, |titlebar, cx| {
88 if let Some(ref menu) = titlebar.application_menu {
89 menu.update(cx, |menu, cx| {
90 menu.navigate_menus_in_direction(ActivateDirection::Right, window, cx)
91 });
92 }
93 });
94 }
95 });
96
97 #[cfg(not(target_os = "macos"))]
98 workspace.register_action(|workspace, _: &ActivateMenuLeft, window, cx| {
99 if let Some(titlebar) = workspace
100 .titlebar_item()
101 .and_then(|item| item.downcast::<TitleBar>().ok())
102 {
103 titlebar.update(cx, |titlebar, cx| {
104 if let Some(ref menu) = titlebar.application_menu {
105 menu.update(cx, |menu, cx| {
106 menu.navigate_menus_in_direction(ActivateDirection::Left, window, cx)
107 });
108 }
109 });
110 }
111 });
112 })
113 .detach();
114}
115
116pub struct TitleBar {
117 platform_style: PlatformStyle,
118 content: Stateful<Div>,
119 children: SmallVec<[AnyElement; 2]>,
120 project: Entity<Project>,
121 user_store: Entity<UserStore>,
122 client: Arc<Client>,
123 workspace: WeakEntity<Workspace>,
124 should_move: bool,
125 application_menu: Option<Entity<ApplicationMenu>>,
126 _subscriptions: Vec<Subscription>,
127 banner: Entity<OnboardingBanner>,
128}
129
130impl Render for TitleBar {
131 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
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 = true;
196 title_bar
197 .when_some(self.application_menu.clone(), |title_bar, menu| {
198 render_project_items = !menu.read(cx).all_menus_shown();
199 title_bar.child(menu)
200 })
201 .when(render_project_items, |title_bar| {
202 title_bar
203 .children(self.render_project_host(cx))
204 .child(self.render_project_name(cx))
205 .children(self.render_project_branch(cx))
206 })
207 })
208 .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()),
209 )
210 .child(self.render_collaborator_list(window, cx))
211 .when(
212 TitleBarSettings::get_global(cx).show_onboarding_banner,
213 |title_bar| title_bar.child(self.banner.clone()),
214 )
215 .child(
216 h_flex()
217 .gap_1()
218 .pr_1()
219 .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
220 .children(self.render_call_controls(window, cx))
221 .map(|el| {
222 let status = self.client.status();
223 let status = &*status.borrow();
224 if matches!(status, client::Status::Connected { .. }) {
225 el.child(self.render_user_menu_button(cx))
226 } else {
227 el.children(self.render_connection_status(status, cx))
228 .child(self.render_sign_in_button(cx))
229 .child(self.render_user_menu_button(cx))
230 }
231 }),
232 ),
233 )
234 .when(!window.is_fullscreen(), |title_bar| {
235 match self.platform_style {
236 PlatformStyle::Mac => title_bar,
237 PlatformStyle::Linux => {
238 if matches!(decorations, Decorations::Client { .. }) {
239 title_bar
240 .child(platform_linux::LinuxWindowControls::new(close_action))
241 .when(supported_controls.window_menu, |titlebar| {
242 titlebar.on_mouse_down(
243 gpui::MouseButton::Right,
244 move |ev, window, _| window.show_window_menu(ev.position),
245 )
246 })
247 .on_mouse_move(cx.listener(move |this, _ev, window, _| {
248 if this.should_move {
249 this.should_move = false;
250 window.start_window_move();
251 }
252 }))
253 .on_mouse_down_out(cx.listener(move |this, _ev, _window, _cx| {
254 this.should_move = false;
255 }))
256 .on_mouse_up(
257 gpui::MouseButton::Left,
258 cx.listener(move |this, _ev, _window, _cx| {
259 this.should_move = false;
260 }),
261 )
262 .on_mouse_down(
263 gpui::MouseButton::Left,
264 cx.listener(move |this, _ev, _window, _cx| {
265 this.should_move = true;
266 }),
267 )
268 } else {
269 title_bar
270 }
271 }
272 PlatformStyle::Windows => {
273 title_bar.child(platform_windows::WindowsWindowControls::new(height))
274 }
275 }
276 })
277 }
278}
279
280impl TitleBar {
281 pub fn new(
282 id: impl Into<ElementId>,
283 workspace: &Workspace,
284 window: &mut Window,
285 cx: &mut Context<Self>,
286 ) -> Self {
287 let project = workspace.project().clone();
288 let user_store = workspace.app_state().user_store.clone();
289 let client = workspace.app_state().client.clone();
290 let active_call = ActiveCall::global(cx);
291
292 let platform_style = PlatformStyle::platform();
293 let application_menu = match platform_style {
294 PlatformStyle::Mac => {
295 if option_env!("ZED_USE_CROSS_PLATFORM_MENU").is_some() {
296 Some(cx.new(|cx| ApplicationMenu::new(window, cx)))
297 } else {
298 None
299 }
300 }
301 PlatformStyle::Linux | PlatformStyle::Windows => {
302 Some(cx.new(|cx| ApplicationMenu::new(window, cx)))
303 }
304 };
305
306 let mut subscriptions = Vec::new();
307 subscriptions.push(
308 cx.observe(&workspace.weak_handle().upgrade().unwrap(), |_, _, cx| {
309 cx.notify()
310 }),
311 );
312 subscriptions.push(cx.subscribe(&project, |_, _, _: &project::Event, cx| cx.notify()));
313 subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx)));
314 subscriptions.push(cx.observe_window_activation(window, Self::window_activation_changed));
315 subscriptions.push(cx.observe(&user_store, |_, _, cx| cx.notify()));
316
317 let banner = cx.new(|cx| {
318 OnboardingBanner::new(
319 "Git Onboarding",
320 IconName::GitBranchSmall,
321 "Git Support",
322 None,
323 zed_actions::OpenGitIntegrationOnboarding.boxed_clone(),
324 cx,
325 )
326 });
327
328 Self {
329 platform_style,
330 content: div().id(id.into()),
331 children: SmallVec::new(),
332 application_menu,
333 workspace: workspace.weak_handle(),
334 should_move: false,
335 project,
336 user_store,
337 client,
338 _subscriptions: subscriptions,
339 banner,
340 }
341 }
342
343 #[cfg(not(target_os = "windows"))]
344 pub fn height(window: &mut Window) -> Pixels {
345 (1.75 * window.rem_size()).max(px(34.))
346 }
347
348 #[cfg(target_os = "windows")]
349 pub fn height(_window: &mut Window) -> Pixels {
350 // todo(windows) instead of hard coded size report the actual size to the Windows platform API
351 px(32.)
352 }
353
354 /// Sets the platform style.
355 pub fn platform_style(mut self, style: PlatformStyle) -> Self {
356 self.platform_style = style;
357 self
358 }
359
360 fn render_ssh_project_host(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
361 let options = self.project.read(cx).ssh_connection_options(cx)?;
362 let host: SharedString = options.connection_string().into();
363
364 let nickname = options
365 .nickname
366 .clone()
367 .map(|nick| nick.into())
368 .unwrap_or_else(|| host.clone());
369
370 let (indicator_color, meta) = match self.project.read(cx).ssh_connection_state(cx)? {
371 remote::ConnectionState::Connecting => (Color::Info, format!("Connecting to: {host}")),
372 remote::ConnectionState::Connected => (Color::Success, format!("Connected to: {host}")),
373 remote::ConnectionState::HeartbeatMissed => (
374 Color::Warning,
375 format!("Connection attempt to {host} missed. Retrying..."),
376 ),
377 remote::ConnectionState::Reconnecting => (
378 Color::Warning,
379 format!("Lost connection to {host}. Reconnecting..."),
380 ),
381 remote::ConnectionState::Disconnected => {
382 (Color::Error, format!("Disconnected from {host}"))
383 }
384 };
385
386 let icon_color = match self.project.read(cx).ssh_connection_state(cx)? {
387 remote::ConnectionState::Connecting => Color::Info,
388 remote::ConnectionState::Connected => Color::Default,
389 remote::ConnectionState::HeartbeatMissed => Color::Warning,
390 remote::ConnectionState::Reconnecting => Color::Warning,
391 remote::ConnectionState::Disconnected => Color::Error,
392 };
393
394 let meta = SharedString::from(meta);
395
396 Some(
397 ButtonLike::new("ssh-server-icon")
398 .child(
399 h_flex()
400 .gap_2()
401 .max_w_32()
402 .child(
403 IconWithIndicator::new(
404 Icon::new(IconName::Server)
405 .size(IconSize::XSmall)
406 .color(icon_color),
407 Some(Indicator::dot().color(indicator_color)),
408 )
409 .indicator_border_color(Some(cx.theme().colors().title_bar_background))
410 .into_any_element(),
411 )
412 .child(
413 Label::new(nickname.clone())
414 .size(LabelSize::Small)
415 .truncate(),
416 ),
417 )
418 .tooltip(move |window, cx| {
419 Tooltip::with_meta(
420 "Remote Project",
421 Some(&OpenRemote),
422 meta.clone(),
423 window,
424 cx,
425 )
426 })
427 .on_click(|_, window, cx| {
428 window.dispatch_action(OpenRemote.boxed_clone(), cx);
429 })
430 .into_any_element(),
431 )
432 }
433
434 pub fn render_project_host(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
435 if self.project.read(cx).is_via_ssh() {
436 return self.render_ssh_project_host(cx);
437 }
438
439 if self.project.read(cx).is_disconnected(cx) {
440 return Some(
441 Button::new("disconnected", "Disconnected")
442 .disabled(true)
443 .color(Color::Disabled)
444 .style(ButtonStyle::Subtle)
445 .label_size(LabelSize::Small)
446 .into_any_element(),
447 );
448 }
449
450 let host = self.project.read(cx).host()?;
451 let host_user = self.user_store.read(cx).get_cached_user(host.user_id)?;
452 let participant_index = self
453 .user_store
454 .read(cx)
455 .participant_indices()
456 .get(&host_user.id)?;
457 Some(
458 Button::new("project_owner_trigger", host_user.github_login.clone())
459 .color(Color::Player(participant_index.0))
460 .style(ButtonStyle::Subtle)
461 .label_size(LabelSize::Small)
462 .tooltip(Tooltip::text(format!(
463 "{} is sharing this project. Click to follow.",
464 host_user.github_login.clone()
465 )))
466 .on_click({
467 let host_peer_id = host.peer_id;
468 cx.listener(move |this, _, window, cx| {
469 this.workspace
470 .update(cx, |workspace, cx| {
471 workspace.follow(host_peer_id, window, cx);
472 })
473 .log_err();
474 })
475 })
476 .into_any_element(),
477 )
478 }
479
480 pub fn render_project_name(&self, cx: &mut Context<Self>) -> impl IntoElement {
481 let name = {
482 let mut names = self.project.read(cx).visible_worktrees(cx).map(|worktree| {
483 let worktree = worktree.read(cx);
484 worktree.root_name()
485 });
486
487 names.next()
488 };
489 let is_project_selected = name.is_some();
490 let name = if let Some(name) = name {
491 util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH)
492 } else {
493 "Open recent project".to_string()
494 };
495
496 Button::new("project_name_trigger", name)
497 .when(!is_project_selected, |b| b.color(Color::Muted))
498 .style(ButtonStyle::Subtle)
499 .label_size(LabelSize::Small)
500 .tooltip(move |window, cx| {
501 Tooltip::for_action(
502 "Recent Projects",
503 &zed_actions::OpenRecent {
504 create_new_window: false,
505 },
506 window,
507 cx,
508 )
509 })
510 .on_click(cx.listener(move |_, _, window, cx| {
511 window.dispatch_action(
512 OpenRecent {
513 create_new_window: false,
514 }
515 .boxed_clone(),
516 cx,
517 );
518 }))
519 }
520
521 pub fn render_project_branch(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
522 let repository = self.project.read(cx).active_repository(cx)?;
523 let workspace = self.workspace.upgrade()?;
524 let branch_name = {
525 let repo = repository.read(cx);
526 repo.branch
527 .as_ref()
528 .map(|branch| branch.name())
529 .map(|name| util::truncate_and_trailoff(&name, MAX_BRANCH_NAME_LENGTH))
530 .or_else(|| {
531 repo.head_commit.as_ref().map(|commit| {
532 commit
533 .sha
534 .chars()
535 .take(MAX_SHORT_SHA_LENGTH)
536 .collect::<String>()
537 })
538 })
539 }?;
540
541 Some(
542 Button::new("project_branch_trigger", branch_name)
543 .color(Color::Muted)
544 .style(ButtonStyle::Subtle)
545 .label_size(LabelSize::Small)
546 .tooltip(move |window, cx| {
547 Tooltip::with_meta(
548 "Recent Branches",
549 Some(&zed_actions::git::Branch),
550 "Local branches only",
551 window,
552 cx,
553 )
554 })
555 .on_click(move |_, window, cx| {
556 let _ = workspace.update(cx, |_this, cx| {
557 window.dispatch_action(zed_actions::git::Branch.boxed_clone(), cx);
558 });
559 })
560 .when(
561 TitleBarSettings::get_global(cx).show_branch_icon,
562 |branch_button| {
563 branch_button
564 .icon(IconName::GitBranch)
565 .icon_position(IconPosition::Start)
566 .icon_color(Color::Muted)
567 },
568 ),
569 )
570 }
571
572 fn window_activation_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
573 if window.is_window_active() {
574 ActiveCall::global(cx)
575 .update(cx, |call, cx| call.set_location(Some(&self.project), cx))
576 .detach_and_log_err(cx);
577 } else if cx.active_window().is_none() {
578 ActiveCall::global(cx)
579 .update(cx, |call, cx| call.set_location(None, cx))
580 .detach_and_log_err(cx);
581 }
582 self.workspace
583 .update(cx, |workspace, cx| {
584 workspace.update_active_view_for_followers(window, cx);
585 })
586 .ok();
587 }
588
589 fn active_call_changed(&mut self, cx: &mut Context<Self>) {
590 cx.notify();
591 }
592
593 fn share_project(&mut self, cx: &mut Context<Self>) {
594 let active_call = ActiveCall::global(cx);
595 let project = self.project.clone();
596 active_call
597 .update(cx, |call, cx| call.share_project(project, cx))
598 .detach_and_log_err(cx);
599 }
600
601 fn unshare_project(&mut self, _: &mut Window, cx: &mut Context<Self>) {
602 let active_call = ActiveCall::global(cx);
603 let project = self.project.clone();
604 active_call
605 .update(cx, |call, cx| call.unshare_project(project, cx))
606 .log_err();
607 }
608
609 fn render_connection_status(
610 &self,
611 status: &client::Status,
612 cx: &mut Context<Self>,
613 ) -> Option<AnyElement> {
614 match status {
615 client::Status::ConnectionError
616 | client::Status::ConnectionLost
617 | client::Status::Reauthenticating { .. }
618 | client::Status::Reconnecting { .. }
619 | client::Status::ReconnectionError { .. } => Some(
620 div()
621 .id("disconnected")
622 .child(Icon::new(IconName::Disconnected).size(IconSize::Small))
623 .tooltip(Tooltip::text("Disconnected"))
624 .into_any_element(),
625 ),
626 client::Status::UpgradeRequired => {
627 let auto_updater = auto_update::AutoUpdater::get(cx);
628 let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) {
629 Some(AutoUpdateStatus::Updated { .. }) => "Please restart Zed to Collaborate",
630 Some(AutoUpdateStatus::Installing)
631 | Some(AutoUpdateStatus::Downloading)
632 | Some(AutoUpdateStatus::Checking) => "Updating...",
633 Some(AutoUpdateStatus::Idle) | Some(AutoUpdateStatus::Errored) | None => {
634 "Please update Zed to Collaborate"
635 }
636 };
637
638 Some(
639 Button::new("connection-status", label)
640 .label_size(LabelSize::Small)
641 .on_click(|_, window, cx| {
642 if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) {
643 if auto_updater.read(cx).status().is_updated() {
644 workspace::reload(&Default::default(), cx);
645 return;
646 }
647 }
648 auto_update::check(&Default::default(), window, cx);
649 })
650 .into_any_element(),
651 )
652 }
653 _ => None,
654 }
655 }
656
657 pub fn render_sign_in_button(&mut self, _: &mut Context<Self>) -> Button {
658 let client = self.client.clone();
659 Button::new("sign_in", "Sign in")
660 .label_size(LabelSize::Small)
661 .on_click(move |_, window, cx| {
662 let client = client.clone();
663 window
664 .spawn(cx, async move |cx| {
665 client
666 .authenticate_and_connect(true, &cx)
667 .await
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.when(cx.has_flag::<NewBillingFeatureFlag>(), |menu| {
683 menu.action(
684 format!(
685 "Current Plan: {}",
686 match plan {
687 None => "",
688 Some(proto::Plan::Free) => "Free",
689 Some(proto::Plan::ZedPro) => "Pro",
690 Some(proto::Plan::ZedProTrial) => "Pro (Trial)",
691 }
692 ),
693 zed_actions::OpenAccountSettings.boxed_clone(),
694 )
695 .separator()
696 })
697 .action("Settings", zed_actions::OpenSettings.boxed_clone())
698 .action("Key Bindings", Box::new(zed_actions::OpenKeymap))
699 .action(
700 "Themes…",
701 zed_actions::theme_selector::Toggle::default().boxed_clone(),
702 )
703 .action(
704 "Icon Themes…",
705 zed_actions::icon_theme_selector::Toggle::default().boxed_clone(),
706 )
707 .action(
708 "Extensions",
709 zed_actions::Extensions::default().boxed_clone(),
710 )
711 .separator()
712 .link(
713 "Book Onboarding",
714 OpenBrowser {
715 url: BOOK_ONBOARDING.to_string(),
716 }
717 .boxed_clone(),
718 )
719 .action("Sign Out", client::SignOut.boxed_clone())
720 })
721 .into()
722 })
723 .trigger_with_tooltip(
724 ButtonLike::new("user-menu")
725 .child(
726 h_flex()
727 .gap_0p5()
728 .children(
729 TitleBarSettings::get_global(cx)
730 .show_user_picture
731 .then(|| Avatar::new(user.avatar_uri.clone())),
732 )
733 .child(
734 Icon::new(IconName::ChevronDown)
735 .size(IconSize::Small)
736 .color(Color::Muted),
737 ),
738 )
739 .style(ButtonStyle::Subtle),
740 Tooltip::text("Toggle User Menu"),
741 )
742 .anchor(gpui::Corner::TopRight)
743 } else {
744 PopoverMenu::new("user-menu")
745 .anchor(Corner::TopRight)
746 .menu(|window, cx| {
747 ContextMenu::build(window, cx, |menu, _, _| {
748 menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
749 .action("Key Bindings", Box::new(zed_actions::OpenKeymap))
750 .action(
751 "Themes…",
752 zed_actions::theme_selector::Toggle::default().boxed_clone(),
753 )
754 .action(
755 "Icon Themes…",
756 zed_actions::icon_theme_selector::Toggle::default().boxed_clone(),
757 )
758 .action(
759 "Extensions",
760 zed_actions::Extensions::default().boxed_clone(),
761 )
762 .separator()
763 .link(
764 "Book Onboarding",
765 OpenBrowser {
766 url: BOOK_ONBOARDING.to_string(),
767 }
768 .boxed_clone(),
769 )
770 })
771 .into()
772 })
773 .trigger_with_tooltip(
774 IconButton::new("user-menu", IconName::ChevronDown).icon_size(IconSize::Small),
775 Tooltip::text("Toggle User Menu"),
776 )
777 }
778 }
779}
780
781impl InteractiveElement for TitleBar {
782 fn interactivity(&mut self) -> &mut Interactivity {
783 self.content.interactivity()
784 }
785}
786
787impl StatefulInteractiveElement for TitleBar {}
788
789impl ParentElement for TitleBar {
790 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
791 self.children.extend(elements)
792 }
793}