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