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 gpui::{
 22    Action, AnyElement, App, Context, Corner, Decorations, Element, Entity, InteractiveElement,
 23    Interactivity, IntoElement, MouseButton, ParentElement, Render, Stateful,
 24    StatefulInteractiveElement, Styled, Subscription, WeakEntity, Window, actions, div, px,
 25};
 26use onboarding_banner::OnboardingBanner;
 27use project::Project;
 28use rpc::proto;
 29use settings::Settings as _;
 30use smallvec::SmallVec;
 31use std::sync::Arc;
 32use theme::ActiveTheme;
 33use ui::{
 34    Avatar, Button, ButtonLike, ButtonStyle, ContextMenu, Icon, IconName, IconSize,
 35    IconWithIndicator, Indicator, PopoverMenu, Tooltip, h_flex, prelude::*,
 36};
 37use util::ResultExt;
 38use workspace::{Workspace, notifications::NotifyResultExt};
 39use zed_actions::{OpenBrowser, OpenRecent, OpenRemote};
 40
 41pub use onboarding_banner::restore_banner;
 42
 43#[cfg(feature = "stories")]
 44pub use stories::*;
 45
 46const MAX_PROJECT_NAME_LENGTH: usize = 40;
 47const MAX_BRANCH_NAME_LENGTH: usize = 40;
 48const MAX_SHORT_SHA_LENGTH: usize = 8;
 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                "Agentic Onboarding",
312                IconName::ZedAssistant,
313                "Agentic Editing",
314                None,
315                zed_actions::agent::OpenOnboardingModal.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 = {
517            let repo = repository.read(cx);
518            repo.branch
519                .as_ref()
520                .map(|branch| branch.name.clone())
521                .map(|name| util::truncate_and_trailoff(&name, MAX_BRANCH_NAME_LENGTH))
522                .or_else(|| {
523                    repo.head_commit.as_ref().map(|commit| {
524                        commit
525                            .sha
526                            .chars()
527                            .take(MAX_SHORT_SHA_LENGTH)
528                            .collect::<String>()
529                    })
530                })
531        }?;
532
533        Some(
534            Button::new("project_branch_trigger", branch_name)
535                .color(Color::Muted)
536                .style(ButtonStyle::Subtle)
537                .label_size(LabelSize::Small)
538                .tooltip(move |window, cx| {
539                    Tooltip::with_meta(
540                        "Recent Branches",
541                        Some(&zed_actions::git::Branch),
542                        "Local branches only",
543                        window,
544                        cx,
545                    )
546                })
547                .on_click(move |_, window, cx| {
548                    let _ = workspace.update(cx, |_this, cx| {
549                        window.dispatch_action(zed_actions::git::Branch.boxed_clone(), cx);
550                    });
551                }),
552        )
553    }
554
555    fn window_activation_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
556        if window.is_window_active() {
557            ActiveCall::global(cx)
558                .update(cx, |call, cx| call.set_location(Some(&self.project), cx))
559                .detach_and_log_err(cx);
560        } else if cx.active_window().is_none() {
561            ActiveCall::global(cx)
562                .update(cx, |call, cx| call.set_location(None, cx))
563                .detach_and_log_err(cx);
564        }
565        self.workspace
566            .update(cx, |workspace, cx| {
567                workspace.update_active_view_for_followers(window, cx);
568            })
569            .ok();
570    }
571
572    fn active_call_changed(&mut self, cx: &mut Context<Self>) {
573        cx.notify();
574    }
575
576    fn share_project(&mut self, cx: &mut Context<Self>) {
577        let active_call = ActiveCall::global(cx);
578        let project = self.project.clone();
579        active_call
580            .update(cx, |call, cx| call.share_project(project, cx))
581            .detach_and_log_err(cx);
582    }
583
584    fn unshare_project(&mut self, _: &mut Window, cx: &mut Context<Self>) {
585        let active_call = ActiveCall::global(cx);
586        let project = self.project.clone();
587        active_call
588            .update(cx, |call, cx| call.unshare_project(project, cx))
589            .log_err();
590    }
591
592    fn render_connection_status(
593        &self,
594        status: &client::Status,
595        cx: &mut Context<Self>,
596    ) -> Option<AnyElement> {
597        match status {
598            client::Status::ConnectionError
599            | client::Status::ConnectionLost
600            | client::Status::Reauthenticating { .. }
601            | client::Status::Reconnecting { .. }
602            | client::Status::ReconnectionError { .. } => Some(
603                div()
604                    .id("disconnected")
605                    .child(Icon::new(IconName::Disconnected).size(IconSize::Small))
606                    .tooltip(Tooltip::text("Disconnected"))
607                    .into_any_element(),
608            ),
609            client::Status::UpgradeRequired => {
610                let auto_updater = auto_update::AutoUpdater::get(cx);
611                let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) {
612                    Some(AutoUpdateStatus::Updated { .. }) => "Please restart Zed to Collaborate",
613                    Some(AutoUpdateStatus::Installing)
614                    | Some(AutoUpdateStatus::Downloading)
615                    | Some(AutoUpdateStatus::Checking) => "Updating...",
616                    Some(AutoUpdateStatus::Idle) | Some(AutoUpdateStatus::Errored) | None => {
617                        "Please update Zed to Collaborate"
618                    }
619                };
620
621                Some(
622                    Button::new("connection-status", label)
623                        .label_size(LabelSize::Small)
624                        .on_click(|_, window, cx| {
625                            if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) {
626                                if auto_updater.read(cx).status().is_updated() {
627                                    workspace::reload(&Default::default(), cx);
628                                    return;
629                                }
630                            }
631                            auto_update::check(&Default::default(), window, cx);
632                        })
633                        .into_any_element(),
634                )
635            }
636            _ => None,
637        }
638    }
639
640    pub fn render_sign_in_button(&mut self, _: &mut Context<Self>) -> Button {
641        let client = self.client.clone();
642        Button::new("sign_in", "Sign in")
643            .label_size(LabelSize::Small)
644            .on_click(move |_, window, cx| {
645                let client = client.clone();
646                window
647                    .spawn(cx, async move |cx| {
648                        client
649                            .authenticate_and_connect(true, &cx)
650                            .await
651                            .notify_async_err(cx);
652                    })
653                    .detach();
654            })
655    }
656
657    pub fn render_user_menu_button(&mut self, cx: &mut Context<Self>) -> impl Element {
658        let user_store = self.user_store.read(cx);
659        if let Some(user) = user_store.current_user() {
660            let plan = user_store.current_plan();
661            PopoverMenu::new("user-menu")
662                .anchor(Corner::TopRight)
663                .menu(move |window, cx| {
664                    ContextMenu::build(window, cx, |menu, _, _cx| {
665                        menu.action(
666                            format!(
667                                "Current Plan: {}",
668                                match plan {
669                                    None => "",
670                                    Some(proto::Plan::Free) => "Free",
671                                    Some(proto::Plan::ZedPro) => "Pro",
672                                    Some(proto::Plan::ZedProTrial) => "Pro (Trial)",
673                                }
674                            ),
675                            zed_actions::OpenAccountSettings.boxed_clone(),
676                        )
677                        .separator()
678                        .action("Settings", zed_actions::OpenSettings.boxed_clone())
679                        .action("Key Bindings", Box::new(zed_actions::OpenKeymap))
680                        .action(
681                            "Themes…",
682                            zed_actions::theme_selector::Toggle::default().boxed_clone(),
683                        )
684                        .action(
685                            "Icon Themes…",
686                            zed_actions::icon_theme_selector::Toggle::default().boxed_clone(),
687                        )
688                        .action(
689                            "Extensions",
690                            zed_actions::Extensions::default().boxed_clone(),
691                        )
692                        .separator()
693                        .link(
694                            "Book Onboarding",
695                            OpenBrowser {
696                                url: BOOK_ONBOARDING.to_string(),
697                            }
698                            .boxed_clone(),
699                        )
700                        .action("Sign Out", client::SignOut.boxed_clone())
701                    })
702                    .into()
703                })
704                .trigger_with_tooltip(
705                    ButtonLike::new("user-menu")
706                        .child(
707                            h_flex()
708                                .gap_0p5()
709                                .children(
710                                    workspace::WorkspaceSettings::get_global(cx)
711                                        .show_user_picture
712                                        .then(|| Avatar::new(user.avatar_uri.clone())),
713                                )
714                                .child(
715                                    Icon::new(IconName::ChevronDown)
716                                        .size(IconSize::Small)
717                                        .color(Color::Muted),
718                                ),
719                        )
720                        .style(ButtonStyle::Subtle),
721                    Tooltip::text("Toggle User Menu"),
722                )
723                .anchor(gpui::Corner::TopRight)
724        } else {
725            PopoverMenu::new("user-menu")
726                .anchor(Corner::TopRight)
727                .menu(|window, cx| {
728                    ContextMenu::build(window, cx, |menu, _, _| {
729                        menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
730                            .action("Key Bindings", Box::new(zed_actions::OpenKeymap))
731                            .action(
732                                "Themes…",
733                                zed_actions::theme_selector::Toggle::default().boxed_clone(),
734                            )
735                            .action(
736                                "Icon Themes…",
737                                zed_actions::icon_theme_selector::Toggle::default().boxed_clone(),
738                            )
739                            .action(
740                                "Extensions",
741                                zed_actions::Extensions::default().boxed_clone(),
742                            )
743                            .separator()
744                            .link(
745                                "Book Onboarding",
746                                OpenBrowser {
747                                    url: BOOK_ONBOARDING.to_string(),
748                                }
749                                .boxed_clone(),
750                            )
751                    })
752                    .into()
753                })
754                .trigger_with_tooltip(
755                    IconButton::new("user-menu", IconName::ChevronDown).icon_size(IconSize::Small),
756                    Tooltip::text("Toggle User Menu"),
757                )
758        }
759    }
760}
761
762impl InteractiveElement for TitleBar {
763    fn interactivity(&mut self) -> &mut Interactivity {
764        self.content.interactivity()
765    }
766}
767
768impl StatefulInteractiveElement for TitleBar {}
769
770impl ParentElement for TitleBar {
771    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
772        self.children.extend(elements)
773    }
774}