title_bar.rs

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