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