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