title_bar.rs

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