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