title_bar.rs

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