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