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 zeta::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                    .items_center()
166                    .justify_between()
167                    .w_full()
168                    // Note: On Windows the title bar behavior is handled by the platform implementation.
169                    .when(self.platform_style != PlatformStyle::Windows, |this| {
170                        this.on_click(|event, window, _| {
171                            if event.up.click_count == 2 {
172                                window.zoom_window();
173                            }
174                        })
175                    })
176                    .child(
177                        h_flex()
178                            .gap_1()
179                            .map(|title_bar| {
180                                let mut render_project_items = true;
181                                title_bar
182                                    .when_some(self.application_menu.clone(), |title_bar, menu| {
183                                        render_project_items = !menu.read(cx).all_menus_shown();
184                                        title_bar.child(menu)
185                                    })
186                                    .when(render_project_items, |title_bar| {
187                                        title_bar
188                                            .children(self.render_project_host(cx))
189                                            .child(self.render_project_name(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(window, cx))
196                    .child(self.zed_predict_banner.clone())
197                    .child(
198                        h_flex()
199                            .gap_1()
200                            .pr_1()
201                            .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
202                            .children(self.render_call_controls(window, cx))
203                            .map(|el| {
204                                let status = self.client.status();
205                                let status = &*status.borrow();
206                                if matches!(status, client::Status::Connected { .. }) {
207                                    el.child(self.render_user_menu_button(cx))
208                                } else {
209                                    el.children(self.render_connection_status(status, cx))
210                                        .child(self.render_sign_in_button(cx))
211                                        .child(self.render_user_menu_button(cx))
212                                }
213                            }),
214                    ),
215            )
216            .when(!window.is_fullscreen(), |title_bar| {
217                match self.platform_style {
218                    PlatformStyle::Mac => title_bar,
219                    PlatformStyle::Linux => {
220                        if matches!(decorations, Decorations::Client { .. }) {
221                            title_bar
222                                .child(platform_linux::LinuxWindowControls::new(close_action))
223                                .when(supported_controls.window_menu, |titlebar| {
224                                    titlebar.on_mouse_down(
225                                        gpui::MouseButton::Right,
226                                        move |ev, window, _| window.show_window_menu(ev.position),
227                                    )
228                                })
229                                .on_mouse_move(cx.listener(move |this, _ev, window, _| {
230                                    if this.should_move {
231                                        this.should_move = false;
232                                        window.start_window_move();
233                                    }
234                                }))
235                                .on_mouse_down_out(cx.listener(move |this, _ev, _window, _cx| {
236                                    this.should_move = false;
237                                }))
238                                .on_mouse_up(
239                                    gpui::MouseButton::Left,
240                                    cx.listener(move |this, _ev, _window, _cx| {
241                                        this.should_move = false;
242                                    }),
243                                )
244                                .on_mouse_down(
245                                    gpui::MouseButton::Left,
246                                    cx.listener(move |this, _ev, _window, _cx| {
247                                        this.should_move = true;
248                                    }),
249                                )
250                        } else {
251                            title_bar
252                        }
253                    }
254                    PlatformStyle::Windows => {
255                        title_bar.child(platform_windows::WindowsWindowControls::new(height))
256                    }
257                }
258            })
259    }
260}
261
262impl TitleBar {
263    pub fn new(
264        id: impl Into<ElementId>,
265        workspace: &Workspace,
266        window: &mut Window,
267        cx: &mut Context<Self>,
268    ) -> Self {
269        let project = workspace.project().clone();
270        let user_store = workspace.app_state().user_store.clone();
271        let client = workspace.app_state().client.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(ZedPredictBanner::new);
300
301        Self {
302            platform_style,
303            content: div().id(id.into()),
304            children: SmallVec::new(),
305            application_menu,
306            workspace: workspace.weak_handle(),
307            should_move: false,
308            project,
309            user_store,
310            client,
311            _subscriptions: subscriptions,
312            zed_predict_banner,
313        }
314    }
315
316    #[cfg(not(target_os = "windows"))]
317    pub fn height(window: &mut Window) -> Pixels {
318        (1.75 * window.rem_size()).max(px(34.))
319    }
320
321    #[cfg(target_os = "windows")]
322    pub fn height(_window: &mut Window) -> Pixels {
323        // todo(windows) instead of hard coded size report the actual size to the Windows platform API
324        px(32.)
325    }
326
327    /// Sets the platform style.
328    pub fn platform_style(mut self, style: PlatformStyle) -> Self {
329        self.platform_style = style;
330        self
331    }
332
333    fn render_ssh_project_host(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
334        let options = self.project.read(cx).ssh_connection_options(cx)?;
335        let host: SharedString = options.connection_string().into();
336
337        let nickname = options
338            .nickname
339            .clone()
340            .map(|nick| nick.into())
341            .unwrap_or_else(|| host.clone());
342
343        let (indicator_color, meta) = match self.project.read(cx).ssh_connection_state(cx)? {
344            remote::ConnectionState::Connecting => (Color::Info, format!("Connecting to: {host}")),
345            remote::ConnectionState::Connected => (Color::Success, format!("Connected to: {host}")),
346            remote::ConnectionState::HeartbeatMissed => (
347                Color::Warning,
348                format!("Connection attempt to {host} missed. Retrying..."),
349            ),
350            remote::ConnectionState::Reconnecting => (
351                Color::Warning,
352                format!("Lost connection to {host}. Reconnecting..."),
353            ),
354            remote::ConnectionState::Disconnected => {
355                (Color::Error, format!("Disconnected from {host}"))
356            }
357        };
358
359        let icon_color = match self.project.read(cx).ssh_connection_state(cx)? {
360            remote::ConnectionState::Connecting => Color::Info,
361            remote::ConnectionState::Connected => Color::Default,
362            remote::ConnectionState::HeartbeatMissed => Color::Warning,
363            remote::ConnectionState::Reconnecting => Color::Warning,
364            remote::ConnectionState::Disconnected => Color::Error,
365        };
366
367        let meta = SharedString::from(meta);
368
369        Some(
370            ButtonLike::new("ssh-server-icon")
371                .child(
372                    h_flex()
373                        .gap_2()
374                        .max_w_32()
375                        .child(
376                            IconWithIndicator::new(
377                                Icon::new(IconName::Server)
378                                    .size(IconSize::XSmall)
379                                    .color(icon_color),
380                                Some(Indicator::dot().color(indicator_color)),
381                            )
382                            .indicator_border_color(Some(cx.theme().colors().title_bar_background))
383                            .into_any_element(),
384                        )
385                        .child(
386                            Label::new(nickname.clone())
387                                .size(LabelSize::Small)
388                                .text_ellipsis(),
389                        ),
390                )
391                .tooltip(move |window, cx| {
392                    Tooltip::with_meta(
393                        "Remote Project",
394                        Some(&OpenRemote),
395                        meta.clone(),
396                        window,
397                        cx,
398                    )
399                })
400                .on_click(|_, window, cx| {
401                    window.dispatch_action(OpenRemote.boxed_clone(), cx);
402                })
403                .into_any_element(),
404        )
405    }
406
407    pub fn render_project_host(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
408        if self.project.read(cx).is_via_ssh() {
409            return self.render_ssh_project_host(cx);
410        }
411
412        if self.project.read(cx).is_disconnected(cx) {
413            return Some(
414                Button::new("disconnected", "Disconnected")
415                    .disabled(true)
416                    .color(Color::Disabled)
417                    .style(ButtonStyle::Subtle)
418                    .label_size(LabelSize::Small)
419                    .into_any_element(),
420            );
421        }
422
423        let host = self.project.read(cx).host()?;
424        let host_user = self.user_store.read(cx).get_cached_user(host.user_id)?;
425        let participant_index = self
426            .user_store
427            .read(cx)
428            .participant_indices()
429            .get(&host_user.id)?;
430        Some(
431            Button::new("project_owner_trigger", host_user.github_login.clone())
432                .color(Color::Player(participant_index.0))
433                .style(ButtonStyle::Subtle)
434                .label_size(LabelSize::Small)
435                .tooltip(Tooltip::text(format!(
436                    "{} is sharing this project. Click to follow.",
437                    host_user.github_login.clone()
438                )))
439                .on_click({
440                    let host_peer_id = host.peer_id;
441                    cx.listener(move |this, _, window, cx| {
442                        this.workspace
443                            .update(cx, |workspace, cx| {
444                                workspace.follow(host_peer_id, window, cx);
445                            })
446                            .log_err();
447                    })
448                })
449                .into_any_element(),
450        )
451    }
452
453    pub fn render_project_name(&self, cx: &mut Context<Self>) -> impl IntoElement {
454        let name = {
455            let mut names = self.project.read(cx).visible_worktrees(cx).map(|worktree| {
456                let worktree = worktree.read(cx);
457                worktree.root_name()
458            });
459
460            names.next()
461        };
462        let is_project_selected = name.is_some();
463        let name = if let Some(name) = name {
464            util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH)
465        } else {
466            "Open recent project".to_string()
467        };
468
469        Button::new("project_name_trigger", name)
470            .when(!is_project_selected, |b| b.color(Color::Muted))
471            .style(ButtonStyle::Subtle)
472            .label_size(LabelSize::Small)
473            .tooltip(move |window, cx| {
474                Tooltip::for_action(
475                    "Recent Projects",
476                    &zed_actions::OpenRecent {
477                        create_new_window: false,
478                    },
479                    window,
480                    cx,
481                )
482            })
483            .on_click(cx.listener(move |_, _, window, cx| {
484                window.dispatch_action(
485                    OpenRecent {
486                        create_new_window: false,
487                    }
488                    .boxed_clone(),
489                    cx,
490                );
491            }))
492    }
493
494    pub fn render_project_branch(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
495        let entry = {
496            let mut names_and_branches =
497                self.project.read(cx).visible_worktrees(cx).map(|worktree| {
498                    let worktree = worktree.read(cx);
499                    worktree.root_git_entry()
500                });
501
502            names_and_branches.next().flatten()
503        };
504        let workspace = self.workspace.upgrade()?;
505        let branch_name = entry
506            .as_ref()
507            .and_then(|entry| entry.branch())
508            .map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH))?;
509        Some(
510            Button::new("project_branch_trigger", branch_name)
511                .color(Color::Muted)
512                .style(ButtonStyle::Subtle)
513                .label_size(LabelSize::Small)
514                .tooltip(move |window, cx| {
515                    Tooltip::with_meta(
516                        "Recent Branches",
517                        Some(&zed_actions::branches::OpenRecent),
518                        "Local branches only",
519                        window,
520                        cx,
521                    )
522                })
523                .on_click(move |_, window, cx| {
524                    let _ = workspace.update(cx, |_this, cx| {
525                        window.dispatch_action(zed_actions::branches::OpenRecent.boxed_clone(), cx);
526                    });
527                }),
528        )
529    }
530
531    fn window_activation_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
532        if window.is_window_active() {
533            ActiveCall::global(cx)
534                .update(cx, |call, cx| call.set_location(Some(&self.project), cx))
535                .detach_and_log_err(cx);
536        } else if cx.active_window().is_none() {
537            ActiveCall::global(cx)
538                .update(cx, |call, cx| call.set_location(None, cx))
539                .detach_and_log_err(cx);
540        }
541        self.workspace
542            .update(cx, |workspace, cx| {
543                workspace.update_active_view_for_followers(window, cx);
544            })
545            .ok();
546    }
547
548    fn active_call_changed(&mut self, cx: &mut Context<Self>) {
549        cx.notify();
550    }
551
552    fn share_project(&mut self, _: &ShareProject, cx: &mut Context<Self>) {
553        let active_call = ActiveCall::global(cx);
554        let project = self.project.clone();
555        active_call
556            .update(cx, |call, cx| call.share_project(project, cx))
557            .detach_and_log_err(cx);
558    }
559
560    fn unshare_project(&mut self, _: &UnshareProject, _: &mut Window, 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.unshare_project(project, cx))
565            .log_err();
566    }
567
568    fn render_connection_status(
569        &self,
570        status: &client::Status,
571        cx: &mut Context<Self>,
572    ) -> Option<AnyElement> {
573        match status {
574            client::Status::ConnectionError
575            | client::Status::ConnectionLost
576            | client::Status::Reauthenticating { .. }
577            | client::Status::Reconnecting { .. }
578            | client::Status::ReconnectionError { .. } => Some(
579                div()
580                    .id("disconnected")
581                    .child(Icon::new(IconName::Disconnected).size(IconSize::Small))
582                    .tooltip(Tooltip::text("Disconnected"))
583                    .into_any_element(),
584            ),
585            client::Status::UpgradeRequired => {
586                let auto_updater = auto_update::AutoUpdater::get(cx);
587                let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) {
588                    Some(AutoUpdateStatus::Updated { .. }) => "Please restart Zed to Collaborate",
589                    Some(AutoUpdateStatus::Installing)
590                    | Some(AutoUpdateStatus::Downloading)
591                    | Some(AutoUpdateStatus::Checking) => "Updating...",
592                    Some(AutoUpdateStatus::Idle) | Some(AutoUpdateStatus::Errored) | None => {
593                        "Please update Zed to Collaborate"
594                    }
595                };
596
597                Some(
598                    Button::new("connection-status", label)
599                        .label_size(LabelSize::Small)
600                        .on_click(|_, window, cx| {
601                            if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) {
602                                if auto_updater.read(cx).status().is_updated() {
603                                    workspace::reload(&Default::default(), cx);
604                                    return;
605                                }
606                            }
607                            auto_update::check(&Default::default(), window, cx);
608                        })
609                        .into_any_element(),
610                )
611            }
612            _ => None,
613        }
614    }
615
616    pub fn render_sign_in_button(&mut self, _: &mut Context<Self>) -> Button {
617        let client = self.client.clone();
618        Button::new("sign_in", "Sign in")
619            .label_size(LabelSize::Small)
620            .on_click(move |_, window, cx| {
621                let client = client.clone();
622                window
623                    .spawn(cx, move |mut cx| async move {
624                        client
625                            .authenticate_and_connect(true, &cx)
626                            .await
627                            .notify_async_err(&mut cx);
628                    })
629                    .detach();
630            })
631    }
632
633    pub fn render_user_menu_button(&mut self, cx: &mut Context<Self>) -> impl Element {
634        let user_store = self.user_store.read(cx);
635        if let Some(user) = user_store.current_user() {
636            let plan = user_store.current_plan();
637            PopoverMenu::new("user-menu")
638                .menu(move |window, cx| {
639                    ContextMenu::build(window, cx, |menu, _, cx| {
640                        menu.when(cx.has_flag::<ZedPro>(), |menu| {
641                            menu.action(
642                                format!(
643                                    "Current Plan: {}",
644                                    match plan {
645                                        None => "",
646                                        Some(proto::Plan::Free) => "Free",
647                                        Some(proto::Plan::ZedPro) => "Pro",
648                                    }
649                                ),
650                                zed_actions::OpenAccountSettings.boxed_clone(),
651                            )
652                            .separator()
653                        })
654                        .action("Settings", zed_actions::OpenSettings.boxed_clone())
655                        .action("Key Bindings", Box::new(zed_actions::OpenKeymap))
656                        .action(
657                            "Themes…",
658                            zed_actions::theme_selector::Toggle::default().boxed_clone(),
659                        )
660                        .action("Extensions", zed_actions::Extensions.boxed_clone())
661                        .separator()
662                        .link(
663                            "Book Onboarding",
664                            OpenBrowser {
665                                url: BOOK_ONBOARDING.to_string(),
666                            }
667                            .boxed_clone(),
668                        )
669                        .action("Sign Out", client::SignOut.boxed_clone())
670                    })
671                    .into()
672                })
673                .trigger(
674                    ButtonLike::new("user-menu")
675                        .child(
676                            h_flex()
677                                .gap_0p5()
678                                .children(
679                                    workspace::WorkspaceSettings::get_global(cx)
680                                        .show_user_picture
681                                        .then(|| Avatar::new(user.avatar_uri.clone())),
682                                )
683                                .child(
684                                    Icon::new(IconName::ChevronDown)
685                                        .size(IconSize::Small)
686                                        .color(Color::Muted),
687                                ),
688                        )
689                        .style(ButtonStyle::Subtle)
690                        .tooltip(Tooltip::text("Toggle User Menu")),
691                )
692                .anchor(gpui::Corner::TopRight)
693        } else {
694            PopoverMenu::new("user-menu")
695                .menu(|window, cx| {
696                    ContextMenu::build(window, cx, |menu, _, _| {
697                        menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
698                            .action("Key Bindings", Box::new(zed_actions::OpenKeymap))
699                            .action(
700                                "Themes…",
701                                zed_actions::theme_selector::Toggle::default().boxed_clone(),
702                            )
703                            .action("Extensions", zed_actions::Extensions.boxed_clone())
704                            .separator()
705                            .link(
706                                "Book Onboarding",
707                                OpenBrowser {
708                                    url: BOOK_ONBOARDING.to_string(),
709                                }
710                                .boxed_clone(),
711                            )
712                    })
713                    .into()
714                })
715                .trigger(
716                    IconButton::new("user-menu", IconName::ChevronDown)
717                        .icon_size(IconSize::Small)
718                        .tooltip(Tooltip::text("Toggle User Menu")),
719                )
720        }
721    }
722}
723
724impl InteractiveElement for TitleBar {
725    fn interactivity(&mut self) -> &mut Interactivity {
726        self.content.interactivity()
727    }
728}
729
730impl StatefulInteractiveElement for TitleBar {}
731
732impl ParentElement for TitleBar {
733    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
734        self.children.extend(elements)
735    }
736}