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