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