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