title_bar.rs

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