title_bar.rs

  1mod application_menu;
  2mod banner;
  3mod collab;
  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 banner::{Banner, BannerDetails};
 20use call::ActiveCall;
 21use client::{Client, UserStore};
 22use feature_flags::{FeatureFlagAppExt, ZedPro};
 23use gpui::{
 24    actions, div, px, Action, AnyElement, App, Context, Corner, Decorations, Element, Entity,
 25    InteractiveElement, Interactivity, IntoElement, MouseButton, ParentElement, Render, Stateful,
 26    StatefulInteractiveElement, Styled, Subscription, WeakEntity, Window,
 27};
 28use project::Project;
 29use rpc::proto;
 30use settings::Settings as _;
 31use smallvec::SmallVec;
 32use std::sync::Arc;
 33use theme::ActiveTheme;
 34use ui::{
 35    h_flex, prelude::*, Avatar, Button, ButtonLike, ButtonStyle, ContextMenu, Icon, IconName,
 36    IconSize, IconWithIndicator, Indicator, PopoverMenu, Tooltip,
 37};
 38use util::ResultExt;
 39use workspace::{notifications::NotifyResultExt, Workspace};
 40use zed_actions::{OpenBrowser, OpenRecent, OpenRemote};
 41
 42pub use 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<Banner>,
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            Banner::new(
320                "Git Onboarding",
321                BannerDetails {
322                    action: zed_actions::OpenGitIntegrationOnboarding.boxed_clone(),
323                    banner_label: Box::new(|_, _| {
324                        h_flex()
325                            .h_full()
326                            .items_center()
327                            .gap_1()
328                            .child(Icon::new(IconName::GitBranchSmall).size(IconSize::Small))
329                            .child(
330                                h_flex()
331                                    .gap_0p5()
332                                    .child(
333                                        Label::new("Introducing:")
334                                            .size(LabelSize::Small)
335                                            .color(Color::Muted),
336                                    )
337                                    .child(Label::new("Git Support").size(LabelSize::Small)),
338                            )
339                            .into_any_element()
340                    }),
341                },
342                cx,
343            )
344        });
345
346        Self {
347            platform_style,
348            content: div().id(id.into()),
349            children: SmallVec::new(),
350            application_menu,
351            workspace: workspace.weak_handle(),
352            should_move: false,
353            project,
354            user_store,
355            client,
356            _subscriptions: subscriptions,
357            banner,
358        }
359    }
360
361    #[cfg(not(target_os = "windows"))]
362    pub fn height(window: &mut Window) -> Pixels {
363        (1.75 * window.rem_size()).max(px(34.))
364    }
365
366    #[cfg(target_os = "windows")]
367    pub fn height(_window: &mut Window) -> Pixels {
368        // todo(windows) instead of hard coded size report the actual size to the Windows platform API
369        px(32.)
370    }
371
372    /// Sets the platform style.
373    pub fn platform_style(mut self, style: PlatformStyle) -> Self {
374        self.platform_style = style;
375        self
376    }
377
378    fn render_ssh_project_host(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
379        let options = self.project.read(cx).ssh_connection_options(cx)?;
380        let host: SharedString = options.connection_string().into();
381
382        let nickname = options
383            .nickname
384            .clone()
385            .map(|nick| nick.into())
386            .unwrap_or_else(|| host.clone());
387
388        let (indicator_color, meta) = match self.project.read(cx).ssh_connection_state(cx)? {
389            remote::ConnectionState::Connecting => (Color::Info, format!("Connecting to: {host}")),
390            remote::ConnectionState::Connected => (Color::Success, format!("Connected to: {host}")),
391            remote::ConnectionState::HeartbeatMissed => (
392                Color::Warning,
393                format!("Connection attempt to {host} missed. Retrying..."),
394            ),
395            remote::ConnectionState::Reconnecting => (
396                Color::Warning,
397                format!("Lost connection to {host}. Reconnecting..."),
398            ),
399            remote::ConnectionState::Disconnected => {
400                (Color::Error, format!("Disconnected from {host}"))
401            }
402        };
403
404        let icon_color = match self.project.read(cx).ssh_connection_state(cx)? {
405            remote::ConnectionState::Connecting => Color::Info,
406            remote::ConnectionState::Connected => Color::Default,
407            remote::ConnectionState::HeartbeatMissed => Color::Warning,
408            remote::ConnectionState::Reconnecting => Color::Warning,
409            remote::ConnectionState::Disconnected => Color::Error,
410        };
411
412        let meta = SharedString::from(meta);
413
414        Some(
415            ButtonLike::new("ssh-server-icon")
416                .child(
417                    h_flex()
418                        .gap_2()
419                        .max_w_32()
420                        .child(
421                            IconWithIndicator::new(
422                                Icon::new(IconName::Server)
423                                    .size(IconSize::XSmall)
424                                    .color(icon_color),
425                                Some(Indicator::dot().color(indicator_color)),
426                            )
427                            .indicator_border_color(Some(cx.theme().colors().title_bar_background))
428                            .into_any_element(),
429                        )
430                        .child(
431                            Label::new(nickname.clone())
432                                .size(LabelSize::Small)
433                                .truncate(),
434                        ),
435                )
436                .tooltip(move |window, cx| {
437                    Tooltip::with_meta(
438                        "Remote Project",
439                        Some(&OpenRemote),
440                        meta.clone(),
441                        window,
442                        cx,
443                    )
444                })
445                .on_click(|_, window, cx| {
446                    window.dispatch_action(OpenRemote.boxed_clone(), cx);
447                })
448                .into_any_element(),
449        )
450    }
451
452    pub fn render_project_host(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
453        if self.project.read(cx).is_via_ssh() {
454            return self.render_ssh_project_host(cx);
455        }
456
457        if self.project.read(cx).is_disconnected(cx) {
458            return Some(
459                Button::new("disconnected", "Disconnected")
460                    .disabled(true)
461                    .color(Color::Disabled)
462                    .style(ButtonStyle::Subtle)
463                    .label_size(LabelSize::Small)
464                    .into_any_element(),
465            );
466        }
467
468        let host = self.project.read(cx).host()?;
469        let host_user = self.user_store.read(cx).get_cached_user(host.user_id)?;
470        let participant_index = self
471            .user_store
472            .read(cx)
473            .participant_indices()
474            .get(&host_user.id)?;
475        Some(
476            Button::new("project_owner_trigger", host_user.github_login.clone())
477                .color(Color::Player(participant_index.0))
478                .style(ButtonStyle::Subtle)
479                .label_size(LabelSize::Small)
480                .tooltip(Tooltip::text(format!(
481                    "{} is sharing this project. Click to follow.",
482                    host_user.github_login.clone()
483                )))
484                .on_click({
485                    let host_peer_id = host.peer_id;
486                    cx.listener(move |this, _, window, cx| {
487                        this.workspace
488                            .update(cx, |workspace, cx| {
489                                workspace.follow(host_peer_id, window, cx);
490                            })
491                            .log_err();
492                    })
493                })
494                .into_any_element(),
495        )
496    }
497
498    pub fn render_project_name(&self, cx: &mut Context<Self>) -> impl IntoElement {
499        let name = {
500            let mut names = self.project.read(cx).visible_worktrees(cx).map(|worktree| {
501                let worktree = worktree.read(cx);
502                worktree.root_name()
503            });
504
505            names.next()
506        };
507        let is_project_selected = name.is_some();
508        let name = if let Some(name) = name {
509            util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH)
510        } else {
511            "Open recent project".to_string()
512        };
513
514        Button::new("project_name_trigger", name)
515            .when(!is_project_selected, |b| b.color(Color::Muted))
516            .style(ButtonStyle::Subtle)
517            .label_size(LabelSize::Small)
518            .tooltip(move |window, cx| {
519                Tooltip::for_action(
520                    "Recent Projects",
521                    &zed_actions::OpenRecent {
522                        create_new_window: false,
523                    },
524                    window,
525                    cx,
526                )
527            })
528            .on_click(cx.listener(move |_, _, window, cx| {
529                window.dispatch_action(
530                    OpenRecent {
531                        create_new_window: false,
532                    }
533                    .boxed_clone(),
534                    cx,
535                );
536            }))
537    }
538
539    pub fn render_project_branch(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
540        let repository = self.project.read(cx).active_repository(cx)?;
541        let workspace = self.workspace.upgrade()?;
542        let branch_name = repository.read(cx).current_branch()?.name.clone();
543        let branch_name = util::truncate_and_trailoff(&branch_name, MAX_BRANCH_NAME_LENGTH);
544        Some(
545            Button::new("project_branch_trigger", branch_name)
546                .color(Color::Muted)
547                .style(ButtonStyle::Subtle)
548                .label_size(LabelSize::Small)
549                .tooltip(move |window, cx| {
550                    Tooltip::with_meta(
551                        "Recent Branches",
552                        Some(&zed_actions::git::Branch),
553                        "Local branches only",
554                        window,
555                        cx,
556                    )
557                })
558                .on_click(move |_, window, cx| {
559                    let _ = workspace.update(cx, |_this, cx| {
560                        window.dispatch_action(zed_actions::git::Branch.boxed_clone(), cx);
561                    });
562                }),
563        )
564    }
565
566    fn window_activation_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
567        if window.is_window_active() {
568            ActiveCall::global(cx)
569                .update(cx, |call, cx| call.set_location(Some(&self.project), cx))
570                .detach_and_log_err(cx);
571        } else if cx.active_window().is_none() {
572            ActiveCall::global(cx)
573                .update(cx, |call, cx| call.set_location(None, cx))
574                .detach_and_log_err(cx);
575        }
576        self.workspace
577            .update(cx, |workspace, cx| {
578                workspace.update_active_view_for_followers(window, cx);
579            })
580            .ok();
581    }
582
583    fn active_call_changed(&mut self, cx: &mut Context<Self>) {
584        cx.notify();
585    }
586
587    fn share_project(&mut self, _: &ShareProject, cx: &mut Context<Self>) {
588        let active_call = ActiveCall::global(cx);
589        let project = self.project.clone();
590        active_call
591            .update(cx, |call, cx| call.share_project(project, cx))
592            .detach_and_log_err(cx);
593    }
594
595    fn unshare_project(&mut self, _: &UnshareProject, _: &mut Window, cx: &mut Context<Self>) {
596        let active_call = ActiveCall::global(cx);
597        let project = self.project.clone();
598        active_call
599            .update(cx, |call, cx| call.unshare_project(project, cx))
600            .log_err();
601    }
602
603    fn render_connection_status(
604        &self,
605        status: &client::Status,
606        cx: &mut Context<Self>,
607    ) -> Option<AnyElement> {
608        match status {
609            client::Status::ConnectionError
610            | client::Status::ConnectionLost
611            | client::Status::Reauthenticating { .. }
612            | client::Status::Reconnecting { .. }
613            | client::Status::ReconnectionError { .. } => Some(
614                div()
615                    .id("disconnected")
616                    .child(Icon::new(IconName::Disconnected).size(IconSize::Small))
617                    .tooltip(Tooltip::text("Disconnected"))
618                    .into_any_element(),
619            ),
620            client::Status::UpgradeRequired => {
621                let auto_updater = auto_update::AutoUpdater::get(cx);
622                let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) {
623                    Some(AutoUpdateStatus::Updated { .. }) => "Please restart Zed to Collaborate",
624                    Some(AutoUpdateStatus::Installing)
625                    | Some(AutoUpdateStatus::Downloading)
626                    | Some(AutoUpdateStatus::Checking) => "Updating...",
627                    Some(AutoUpdateStatus::Idle) | Some(AutoUpdateStatus::Errored) | None => {
628                        "Please update Zed to Collaborate"
629                    }
630                };
631
632                Some(
633                    Button::new("connection-status", label)
634                        .label_size(LabelSize::Small)
635                        .on_click(|_, window, cx| {
636                            if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) {
637                                if auto_updater.read(cx).status().is_updated() {
638                                    workspace::reload(&Default::default(), cx);
639                                    return;
640                                }
641                            }
642                            auto_update::check(&Default::default(), window, cx);
643                        })
644                        .into_any_element(),
645                )
646            }
647            _ => None,
648        }
649    }
650
651    pub fn render_sign_in_button(&mut self, _: &mut Context<Self>) -> Button {
652        let client = self.client.clone();
653        Button::new("sign_in", "Sign in")
654            .label_size(LabelSize::Small)
655            .on_click(move |_, window, cx| {
656                let client = client.clone();
657                window
658                    .spawn(cx, async move |cx| {
659                        client
660                            .authenticate_and_connect(true, &cx)
661                            .await
662                            .notify_async_err(cx);
663                    })
664                    .detach();
665            })
666    }
667
668    pub fn render_user_menu_button(&mut self, cx: &mut Context<Self>) -> impl Element {
669        let user_store = self.user_store.read(cx);
670        if let Some(user) = user_store.current_user() {
671            let plan = user_store.current_plan();
672            PopoverMenu::new("user-menu")
673                .anchor(Corner::TopRight)
674                .menu(move |window, cx| {
675                    ContextMenu::build(window, cx, |menu, _, cx| {
676                        menu.when(cx.has_flag::<ZedPro>(), |menu| {
677                            menu.action(
678                                format!(
679                                    "Current Plan: {}",
680                                    match plan {
681                                        None => "",
682                                        Some(proto::Plan::Free) => "Free",
683                                        Some(proto::Plan::ZedPro) => "Pro",
684                                    }
685                                ),
686                                zed_actions::OpenAccountSettings.boxed_clone(),
687                            )
688                            .separator()
689                        })
690                        .action("Settings", zed_actions::OpenSettings.boxed_clone())
691                        .action("Key Bindings", Box::new(zed_actions::OpenKeymap))
692                        .action(
693                            "Themes…",
694                            zed_actions::theme_selector::Toggle::default().boxed_clone(),
695                        )
696                        .action(
697                            "Icon Themes…",
698                            zed_actions::icon_theme_selector::Toggle::default().boxed_clone(),
699                        )
700                        .action(
701                            "Extensions",
702                            zed_actions::Extensions::default().boxed_clone(),
703                        )
704                        .separator()
705                        .link(
706                            "Book Onboarding",
707                            OpenBrowser {
708                                url: BOOK_ONBOARDING.to_string(),
709                            }
710                            .boxed_clone(),
711                        )
712                        .action("Sign Out", client::SignOut.boxed_clone())
713                    })
714                    .into()
715                })
716                .trigger_with_tooltip(
717                    ButtonLike::new("user-menu")
718                        .child(
719                            h_flex()
720                                .gap_0p5()
721                                .children(
722                                    workspace::WorkspaceSettings::get_global(cx)
723                                        .show_user_picture
724                                        .then(|| Avatar::new(user.avatar_uri.clone())),
725                                )
726                                .child(
727                                    Icon::new(IconName::ChevronDown)
728                                        .size(IconSize::Small)
729                                        .color(Color::Muted),
730                                ),
731                        )
732                        .style(ButtonStyle::Subtle),
733                    Tooltip::text("Toggle User Menu"),
734                )
735                .anchor(gpui::Corner::TopRight)
736        } else {
737            PopoverMenu::new("user-menu")
738                .anchor(Corner::TopRight)
739                .menu(|window, cx| {
740                    ContextMenu::build(window, cx, |menu, _, _| {
741                        menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
742                            .action("Key Bindings", Box::new(zed_actions::OpenKeymap))
743                            .action(
744                                "Themes…",
745                                zed_actions::theme_selector::Toggle::default().boxed_clone(),
746                            )
747                            .action(
748                                "Icon Themes…",
749                                zed_actions::icon_theme_selector::Toggle::default().boxed_clone(),
750                            )
751                            .action(
752                                "Extensions",
753                                zed_actions::Extensions::default().boxed_clone(),
754                            )
755                            .separator()
756                            .link(
757                                "Book Onboarding",
758                                OpenBrowser {
759                                    url: BOOK_ONBOARDING.to_string(),
760                                }
761                                .boxed_clone(),
762                            )
763                    })
764                    .into()
765                })
766                .trigger_with_tooltip(
767                    IconButton::new("user-menu", IconName::ChevronDown).icon_size(IconSize::Small),
768                    Tooltip::text("Toggle User Menu"),
769                )
770        }
771    }
772}
773
774impl InteractiveElement for TitleBar {
775    fn interactivity(&mut self) -> &mut Interactivity {
776        self.content.interactivity()
777    }
778}
779
780impl StatefulInteractiveElement for TitleBar {}
781
782impl ParentElement for TitleBar {
783    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
784        self.children.extend(elements)
785    }
786}