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