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