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