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