1mod application_menu;
2mod collab;
3mod platforms;
4mod window_controls;
5
6#[cfg(feature = "stories")]
7mod stories;
8
9use crate::application_menu::ApplicationMenu;
10
11#[cfg(not(target_os = "macos"))]
12use crate::application_menu::{NavigateApplicationMenuInDirection, OpenApplicationMenu};
13
14use crate::platforms::{platform_linux, platform_mac, platform_windows};
15use auto_update::AutoUpdateStatus;
16use call::ActiveCall;
17use client::{Client, UserStore};
18use feature_flags::{FeatureFlagAppExt, GitUiFeatureFlag, ZedPro};
19use git_ui::repository_selector::RepositorySelector;
20use git_ui::repository_selector::RepositorySelectorPopoverMenu;
21use gpui::{
22 actions, div, px, Action, AnyElement, AppContext, Decorations, Element, InteractiveElement,
23 Interactivity, IntoElement, Model, MouseButton, ParentElement, Render, Stateful,
24 StatefulInteractiveElement, Styled, Subscription, View, ViewContext, VisualContext, WeakView,
25};
26use project::Project;
27use rpc::proto;
28use settings::Settings as _;
29use smallvec::SmallVec;
30use std::sync::atomic::{AtomicBool, Ordering};
31use std::sync::Arc;
32use theme::ActiveTheme;
33use ui::{
34 h_flex, prelude::*, Avatar, Button, ButtonLike, ButtonStyle, ContextMenu, Icon, IconName,
35 IconSize, IconWithIndicator, Indicator, PopoverMenu, Tooltip,
36};
37use util::ResultExt;
38use workspace::{notifications::NotifyResultExt, Workspace};
39use zed_actions::{OpenBrowser, OpenRecent, OpenRemote};
40
41#[cfg(feature = "stories")]
42pub use stories::*;
43
44const MAX_PROJECT_NAME_LENGTH: usize = 40;
45const MAX_BRANCH_NAME_LENGTH: usize = 40;
46
47const BOOK_ONBOARDING: &str = "https://dub.sh/zed-c-onboarding";
48
49actions!(
50 collab,
51 [
52 ShareProject,
53 UnshareProject,
54 ToggleUserMenu,
55 ToggleProjectMenu,
56 SwitchBranch
57 ]
58);
59
60pub fn init(cx: &mut AppContext) {
61 cx.observe_new_views(|workspace: &mut Workspace, cx| {
62 let item = cx.new_view(|cx| TitleBar::new("title-bar", workspace, cx));
63 workspace.set_titlebar_item(item.into(), cx);
64
65 #[cfg(not(target_os = "macos"))]
66 workspace.register_action(|workspace, action: &OpenApplicationMenu, cx| {
67 if let Some(titlebar) = workspace
68 .titlebar_item()
69 .and_then(|item| item.downcast::<TitleBar>().ok())
70 {
71 titlebar.update(cx, |titlebar, cx| {
72 if let Some(ref menu) = titlebar.application_menu {
73 menu.update(cx, |menu, cx| menu.open_menu(action, cx));
74 }
75 });
76 }
77 });
78
79 #[cfg(not(target_os = "macos"))]
80 workspace.register_action(
81 |workspace, action: &NavigateApplicationMenuInDirection, 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(action, cx)
90 });
91 }
92 });
93 }
94 },
95 );
96 })
97 .detach();
98}
99
100pub struct TitleBar {
101 platform_style: PlatformStyle,
102 content: Stateful<Div>,
103 children: SmallVec<[AnyElement; 2]>,
104 repository_selector: View<RepositorySelector>,
105 project: Model<Project>,
106 user_store: Model<UserStore>,
107 client: Arc<Client>,
108 workspace: WeakView<Workspace>,
109 should_move: bool,
110 application_menu: Option<View<ApplicationMenu>>,
111 _subscriptions: Vec<Subscription>,
112 git_ui_enabled: Arc<AtomicBool>,
113}
114
115impl Render for TitleBar {
116 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
117 let close_action = Box::new(workspace::CloseWindow);
118 let height = Self::height(cx);
119 let supported_controls = cx.window_controls();
120 let decorations = cx.window_decorations();
121 let titlebar_color = if cfg!(any(target_os = "linux", target_os = "freebsd")) {
122 if cx.is_window_active() && !self.should_move {
123 cx.theme().colors().title_bar_background
124 } else {
125 cx.theme().colors().title_bar_inactive_background
126 }
127 } else {
128 cx.theme().colors().title_bar_background
129 };
130
131 h_flex()
132 .id("titlebar")
133 .w_full()
134 .h(height)
135 .map(|this| {
136 if cx.is_fullscreen() {
137 this.pl_2()
138 } else if self.platform_style == PlatformStyle::Mac {
139 this.pl(px(platform_mac::TRAFFIC_LIGHT_PADDING))
140 } else {
141 this.pl_2()
142 }
143 })
144 .map(|el| match decorations {
145 Decorations::Server => el,
146 Decorations::Client { tiling, .. } => el
147 .when(!(tiling.top || tiling.right), |el| {
148 el.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
149 })
150 .when(!(tiling.top || tiling.left), |el| {
151 el.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
152 })
153 // this border is to avoid a transparent gap in the rounded corners
154 .mt(px(-1.))
155 .border(px(1.))
156 .border_color(titlebar_color),
157 })
158 .bg(titlebar_color)
159 .content_stretch()
160 .child(
161 div()
162 .id("titlebar-content")
163 .flex()
164 .flex_row()
165 .justify_between()
166 .w_full()
167 // Note: On Windows the title bar behavior is handled by the platform implementation.
168 .when(self.platform_style != PlatformStyle::Windows, |this| {
169 this.on_click(|event, cx| {
170 if event.up.click_count == 2 {
171 cx.zoom_window();
172 }
173 })
174 })
175 .child(
176 h_flex()
177 .gap_1()
178 .map(|title_bar| {
179 let mut render_project_items = true;
180 title_bar
181 .when_some(self.application_menu.clone(), |title_bar, menu| {
182 render_project_items = !menu.read(cx).all_menus_shown();
183 title_bar.child(menu)
184 })
185 .when(render_project_items, |title_bar| {
186 title_bar
187 .children(self.render_project_host(cx))
188 .child(self.render_project_name(cx))
189 .children(self.render_current_repository(cx))
190 .children(self.render_project_branch(cx))
191 })
192 })
193 .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation()),
194 )
195 .child(self.render_collaborator_list(cx))
196 .child(
197 h_flex()
198 .gap_1()
199 .pr_1()
200 .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
201 .children(self.render_call_controls(cx))
202 .map(|el| {
203 let status = self.client.status();
204 let status = &*status.borrow();
205 if matches!(status, client::Status::Connected { .. }) {
206 el.child(self.render_user_menu_button(cx))
207 } else {
208 el.children(self.render_connection_status(status, cx))
209 .child(self.render_sign_in_button(cx))
210 .child(self.render_user_menu_button(cx))
211 }
212 }),
213 ),
214 )
215 .when(!cx.is_fullscreen(), |title_bar| match self.platform_style {
216 PlatformStyle::Mac => title_bar,
217 PlatformStyle::Linux => {
218 if matches!(decorations, Decorations::Client { .. }) {
219 title_bar
220 .child(platform_linux::LinuxWindowControls::new(close_action))
221 .when(supported_controls.window_menu, |titlebar| {
222 titlebar.on_mouse_down(gpui::MouseButton::Right, move |ev, cx| {
223 cx.show_window_menu(ev.position)
224 })
225 })
226 .on_mouse_move(cx.listener(move |this, _ev, cx| {
227 if this.should_move {
228 this.should_move = false;
229 cx.start_window_move();
230 }
231 }))
232 .on_mouse_down_out(cx.listener(move |this, _ev, _cx| {
233 this.should_move = false;
234 }))
235 .on_mouse_up(
236 gpui::MouseButton::Left,
237 cx.listener(move |this, _ev, _cx| {
238 this.should_move = false;
239 }),
240 )
241 .on_mouse_down(
242 gpui::MouseButton::Left,
243 cx.listener(move |this, _ev, _cx| {
244 this.should_move = true;
245 }),
246 )
247 } else {
248 title_bar
249 }
250 }
251 PlatformStyle::Windows => {
252 title_bar.child(platform_windows::WindowsWindowControls::new(height))
253 }
254 })
255 }
256}
257
258impl TitleBar {
259 pub fn new(
260 id: impl Into<ElementId>,
261 workspace: &Workspace,
262 cx: &mut ViewContext<Self>,
263 ) -> Self {
264 let project = workspace.project().clone();
265 let user_store = workspace.app_state().user_store.clone();
266 let client = workspace.app_state().client.clone();
267 let active_call = ActiveCall::global(cx);
268
269 let platform_style = PlatformStyle::platform();
270 let application_menu = match platform_style {
271 PlatformStyle::Mac => {
272 if option_env!("ZED_USE_CROSS_PLATFORM_MENU").is_some() {
273 Some(cx.new_view(ApplicationMenu::new))
274 } else {
275 None
276 }
277 }
278 PlatformStyle::Linux | PlatformStyle::Windows => {
279 Some(cx.new_view(ApplicationMenu::new))
280 }
281 };
282
283 let mut subscriptions = Vec::new();
284 subscriptions.push(
285 cx.observe(&workspace.weak_handle().upgrade().unwrap(), |_, _, cx| {
286 cx.notify()
287 }),
288 );
289 subscriptions.push(cx.observe(&project, |_, _, cx| cx.notify()));
290 subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx)));
291 subscriptions.push(cx.observe_window_activation(Self::window_activation_changed));
292 subscriptions.push(cx.observe(&user_store, |_, _, cx| cx.notify()));
293
294 let is_git_ui_enabled = Arc::new(AtomicBool::new(false));
295 subscriptions.push(cx.observe_flag::<GitUiFeatureFlag, _>({
296 let is_git_ui_enabled = is_git_ui_enabled.clone();
297 move |enabled, _cx| {
298 is_git_ui_enabled.store(enabled, Ordering::SeqCst);
299 }
300 }));
301
302 Self {
303 platform_style,
304 content: div().id(id.into()),
305 children: SmallVec::new(),
306 application_menu,
307 repository_selector: cx.new_view(|cx| RepositorySelector::new(project.clone(), cx)),
308 workspace: workspace.weak_handle(),
309 should_move: false,
310 project,
311 user_store,
312 client,
313 _subscriptions: subscriptions,
314 git_ui_enabled: is_git_ui_enabled,
315 }
316 }
317
318 #[cfg(not(target_os = "windows"))]
319 pub fn height(cx: &mut WindowContext) -> Pixels {
320 (1.75 * cx.rem_size()).max(px(34.))
321 }
322
323 #[cfg(target_os = "windows")]
324 pub fn height(_cx: &mut WindowContext) -> Pixels {
325 // todo(windows) instead of hard coded size report the actual size to the Windows platform API
326 px(32.)
327 }
328
329 /// Sets the platform style.
330 pub fn platform_style(mut self, style: PlatformStyle) -> Self {
331 self.platform_style = style;
332 self
333 }
334
335 fn render_ssh_project_host(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
336 let options = self.project.read(cx).ssh_connection_options(cx)?;
337 let host: SharedString = options.connection_string().into();
338
339 let nickname = options
340 .nickname
341 .clone()
342 .map(|nick| nick.into())
343 .unwrap_or_else(|| host.clone());
344
345 let (indicator_color, meta) = match self.project.read(cx).ssh_connection_state(cx)? {
346 remote::ConnectionState::Connecting => (Color::Info, format!("Connecting to: {host}")),
347 remote::ConnectionState::Connected => (Color::Success, format!("Connected to: {host}")),
348 remote::ConnectionState::HeartbeatMissed => (
349 Color::Warning,
350 format!("Connection attempt to {host} missed. Retrying..."),
351 ),
352 remote::ConnectionState::Reconnecting => (
353 Color::Warning,
354 format!("Lost connection to {host}. Reconnecting..."),
355 ),
356 remote::ConnectionState::Disconnected => {
357 (Color::Error, format!("Disconnected from {host}"))
358 }
359 };
360
361 let icon_color = match self.project.read(cx).ssh_connection_state(cx)? {
362 remote::ConnectionState::Connecting => Color::Info,
363 remote::ConnectionState::Connected => Color::Default,
364 remote::ConnectionState::HeartbeatMissed => Color::Warning,
365 remote::ConnectionState::Reconnecting => Color::Warning,
366 remote::ConnectionState::Disconnected => Color::Error,
367 };
368
369 let meta = SharedString::from(meta);
370
371 Some(
372 ButtonLike::new("ssh-server-icon")
373 .child(
374 h_flex()
375 .gap_2()
376 .max_w_32()
377 .child(
378 IconWithIndicator::new(
379 Icon::new(IconName::Server)
380 .size(IconSize::XSmall)
381 .color(icon_color),
382 Some(Indicator::dot().color(indicator_color)),
383 )
384 .indicator_border_color(Some(cx.theme().colors().title_bar_background))
385 .into_any_element(),
386 )
387 .child(
388 Label::new(nickname.clone())
389 .size(LabelSize::Small)
390 .text_ellipsis(),
391 ),
392 )
393 .tooltip(move |cx| {
394 Tooltip::with_meta("Remote Project", Some(&OpenRemote), meta.clone(), cx)
395 })
396 .on_click(|_, cx| {
397 cx.dispatch_action(OpenRemote.boxed_clone());
398 })
399 .into_any_element(),
400 )
401 }
402
403 pub fn render_project_host(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
404 if self.project.read(cx).is_via_ssh() {
405 return self.render_ssh_project_host(cx);
406 }
407
408 if self.project.read(cx).is_disconnected(cx) {
409 return Some(
410 Button::new("disconnected", "Disconnected")
411 .disabled(true)
412 .color(Color::Disabled)
413 .style(ButtonStyle::Subtle)
414 .label_size(LabelSize::Small)
415 .into_any_element(),
416 );
417 }
418
419 let host = self.project.read(cx).host()?;
420 let host_user = self.user_store.read(cx).get_cached_user(host.user_id)?;
421 let participant_index = self
422 .user_store
423 .read(cx)
424 .participant_indices()
425 .get(&host_user.id)?;
426 Some(
427 Button::new("project_owner_trigger", host_user.github_login.clone())
428 .color(Color::Player(participant_index.0))
429 .style(ButtonStyle::Subtle)
430 .label_size(LabelSize::Small)
431 .tooltip(move |cx| {
432 Tooltip::text(
433 format!(
434 "{} is sharing this project. Click to follow.",
435 host_user.github_login.clone()
436 ),
437 cx,
438 )
439 })
440 .on_click({
441 let host_peer_id = host.peer_id;
442 cx.listener(move |this, _, cx| {
443 this.workspace
444 .update(cx, |workspace, cx| {
445 workspace.follow(host_peer_id, cx);
446 })
447 .log_err();
448 })
449 })
450 .into_any_element(),
451 )
452 }
453
454 pub fn render_project_name(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
455 let name = {
456 let mut names = self.project.read(cx).visible_worktrees(cx).map(|worktree| {
457 let worktree = worktree.read(cx);
458 worktree.root_name()
459 });
460
461 names.next()
462 };
463 let is_project_selected = name.is_some();
464 let name = if let Some(name) = name {
465 util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH)
466 } else {
467 "Open recent project".to_string()
468 };
469
470 Button::new("project_name_trigger", name)
471 .when(!is_project_selected, |b| b.color(Color::Muted))
472 .style(ButtonStyle::Subtle)
473 .label_size(LabelSize::Small)
474 .tooltip(move |cx| {
475 Tooltip::for_action(
476 "Recent Projects",
477 &zed_actions::OpenRecent {
478 create_new_window: false,
479 },
480 cx,
481 )
482 })
483 .on_click(cx.listener(move |_, _, cx| {
484 cx.dispatch_action(
485 OpenRecent {
486 create_new_window: false,
487 }
488 .boxed_clone(),
489 );
490 }))
491 }
492
493 // NOTE: Not sure we want to keep this in the titlebar, but for while we are working on Git it is helpful in the short term
494 pub fn render_current_repository(
495 &self,
496 cx: &mut ViewContext<Self>,
497 ) -> Option<impl IntoElement> {
498 if !self.git_ui_enabled.load(Ordering::SeqCst) {
499 return None;
500 }
501
502 let active_repository = self.project.read(cx).active_repository(cx)?;
503 let display_name = active_repository.display_name(self.project.read(cx), cx);
504
505 // TODO: what to render if no active repository?
506 Some(RepositorySelectorPopoverMenu::new(
507 self.repository_selector.clone(),
508 ButtonLike::new("active-repository")
509 .style(ButtonStyle::Subtle)
510 .child(
511 h_flex().w_full().gap_0p5().child(
512 div()
513 .overflow_x_hidden()
514 .flex_grow()
515 .whitespace_nowrap()
516 .child(
517 h_flex()
518 .gap_1()
519 .child(
520 Label::new(display_name)
521 .size(LabelSize::Small)
522 .color(Color::Muted),
523 )
524 .into_any_element(),
525 ),
526 ),
527 ),
528 ))
529 }
530
531 pub fn render_project_branch(&self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
532 let entry = {
533 let mut names_and_branches =
534 self.project.read(cx).visible_worktrees(cx).map(|worktree| {
535 let worktree = worktree.read(cx);
536 worktree.root_git_entry()
537 });
538
539 names_and_branches.next().flatten()
540 };
541 let workspace = self.workspace.upgrade()?;
542 let branch_name = entry
543 .as_ref()
544 .and_then(|entry| entry.branch())
545 .map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH))?;
546 Some(
547 Button::new("project_branch_trigger", branch_name)
548 .color(Color::Muted)
549 .style(ButtonStyle::Subtle)
550 .label_size(LabelSize::Small)
551 .tooltip(move |cx| {
552 Tooltip::with_meta(
553 "Recent Branches",
554 Some(&zed_actions::branches::OpenRecent),
555 "Local branches only",
556 cx,
557 )
558 })
559 .on_click(move |_, cx| {
560 let _ = workspace.update(cx, |_this, cx| {
561 cx.dispatch_action(zed_actions::branches::OpenRecent.boxed_clone());
562 });
563 }),
564 )
565 }
566
567 fn window_activation_changed(&mut self, cx: &mut ViewContext<Self>) {
568 if cx.is_window_active() {
569 ActiveCall::global(cx)
570 .update(cx, |call, cx| call.set_location(Some(&self.project), cx))
571 .detach_and_log_err(cx);
572 } else if cx.active_window().is_none() {
573 ActiveCall::global(cx)
574 .update(cx, |call, cx| call.set_location(None, cx))
575 .detach_and_log_err(cx);
576 }
577 self.workspace
578 .update(cx, |workspace, cx| {
579 workspace.update_active_view_for_followers(cx);
580 })
581 .ok();
582 }
583
584 fn active_call_changed(&mut self, cx: &mut ViewContext<Self>) {
585 cx.notify();
586 }
587
588 fn share_project(&mut self, _: &ShareProject, cx: &mut ViewContext<Self>) {
589 let active_call = ActiveCall::global(cx);
590 let project = self.project.clone();
591 active_call
592 .update(cx, |call, cx| call.share_project(project, cx))
593 .detach_and_log_err(cx);
594 }
595
596 fn unshare_project(&mut self, _: &UnshareProject, cx: &mut ViewContext<Self>) {
597 let active_call = ActiveCall::global(cx);
598 let project = self.project.clone();
599 active_call
600 .update(cx, |call, cx| call.unshare_project(project, cx))
601 .log_err();
602 }
603
604 fn render_connection_status(
605 &self,
606 status: &client::Status,
607 cx: &mut ViewContext<Self>,
608 ) -> Option<AnyElement> {
609 match status {
610 client::Status::ConnectionError
611 | client::Status::ConnectionLost
612 | client::Status::Reauthenticating { .. }
613 | client::Status::Reconnecting { .. }
614 | client::Status::ReconnectionError { .. } => Some(
615 div()
616 .id("disconnected")
617 .child(Icon::new(IconName::Disconnected).size(IconSize::Small))
618 .tooltip(|cx| Tooltip::text("Disconnected", cx))
619 .into_any_element(),
620 ),
621 client::Status::UpgradeRequired => {
622 let auto_updater = auto_update::AutoUpdater::get(cx);
623 let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) {
624 Some(AutoUpdateStatus::Updated { .. }) => "Please restart Zed to Collaborate",
625 Some(AutoUpdateStatus::Installing)
626 | Some(AutoUpdateStatus::Downloading)
627 | Some(AutoUpdateStatus::Checking) => "Updating...",
628 Some(AutoUpdateStatus::Idle) | Some(AutoUpdateStatus::Errored) | None => {
629 "Please update Zed to Collaborate"
630 }
631 };
632
633 Some(
634 Button::new("connection-status", label)
635 .label_size(LabelSize::Small)
636 .on_click(|_, cx| {
637 if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) {
638 if auto_updater.read(cx).status().is_updated() {
639 workspace::reload(&Default::default(), cx);
640 return;
641 }
642 }
643 auto_update::check(&Default::default(), cx);
644 })
645 .into_any_element(),
646 )
647 }
648 _ => None,
649 }
650 }
651
652 pub fn render_sign_in_button(&mut self, _: &mut ViewContext<Self>) -> Button {
653 let client = self.client.clone();
654 Button::new("sign_in", "Sign in")
655 .label_size(LabelSize::Small)
656 .on_click(move |_, cx| {
657 let client = client.clone();
658 cx.spawn(move |mut cx| async move {
659 client
660 .authenticate_and_connect(true, &cx)
661 .await
662 .notify_async_err(&mut cx);
663 })
664 .detach();
665 })
666 }
667
668 pub fn render_user_menu_button(&mut self, cx: &mut ViewContext<Self>) -> impl Element {
669 let user_store = self.user_store.read(cx);
670 if let Some(user) = user_store.current_user() {
671 let plan = user_store.current_plan();
672 PopoverMenu::new("user-menu")
673 .menu(move |cx| {
674 ContextMenu::build(cx, |menu, cx| {
675 menu.when(cx.has_flag::<ZedPro>(), |menu| {
676 menu.action(
677 format!(
678 "Current Plan: {}",
679 match plan {
680 None => "",
681 Some(proto::Plan::Free) => "Free",
682 Some(proto::Plan::ZedPro) => "Pro",
683 }
684 ),
685 zed_actions::OpenAccountSettings.boxed_clone(),
686 )
687 .separator()
688 })
689 .action("Settings", zed_actions::OpenSettings.boxed_clone())
690 .action("Key Bindings", Box::new(zed_actions::OpenKeymap))
691 .action(
692 "Themes…",
693 zed_actions::theme_selector::Toggle::default().boxed_clone(),
694 )
695 .action("Extensions", zed_actions::Extensions.boxed_clone())
696 .separator()
697 .link(
698 "Book Onboarding",
699 OpenBrowser {
700 url: BOOK_ONBOARDING.to_string(),
701 }
702 .boxed_clone(),
703 )
704 .action("Sign Out", client::SignOut.boxed_clone())
705 })
706 .into()
707 })
708 .trigger(
709 ButtonLike::new("user-menu")
710 .child(
711 h_flex()
712 .gap_0p5()
713 .children(
714 workspace::WorkspaceSettings::get_global(cx)
715 .show_user_picture
716 .then(|| Avatar::new(user.avatar_uri.clone())),
717 )
718 .child(
719 Icon::new(IconName::ChevronDown)
720 .size(IconSize::Small)
721 .color(Color::Muted),
722 ),
723 )
724 .style(ButtonStyle::Subtle)
725 .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),
726 )
727 .anchor(gpui::Corner::TopRight)
728 } else {
729 PopoverMenu::new("user-menu")
730 .menu(|cx| {
731 ContextMenu::build(cx, |menu, _| {
732 menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
733 .action("Key Bindings", Box::new(zed_actions::OpenKeymap))
734 .action(
735 "Themes…",
736 zed_actions::theme_selector::Toggle::default().boxed_clone(),
737 )
738 .action("Extensions", zed_actions::Extensions.boxed_clone())
739 .separator()
740 .link(
741 "Book Onboarding",
742 OpenBrowser {
743 url: BOOK_ONBOARDING.to_string(),
744 }
745 .boxed_clone(),
746 )
747 })
748 .into()
749 })
750 .trigger(
751 ButtonLike::new("user-menu")
752 .child(
753 h_flex().gap_0p5().child(
754 Icon::new(IconName::ChevronDown)
755 .size(IconSize::Small)
756 .color(Color::Muted),
757 ),
758 )
759 .style(ButtonStyle::Subtle)
760 .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),
761 )
762 }
763 }
764}
765
766impl InteractiveElement for TitleBar {
767 fn interactivity(&mut self) -> &mut Interactivity {
768 self.content.interactivity()
769 }
770}
771
772impl StatefulInteractiveElement for TitleBar {}
773
774impl ParentElement for TitleBar {
775 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
776 self.children.extend(elements)
777 }
778}