dock.rs

  1use crate::{StatusItemView, Workspace, WorkspaceBounds};
  2use context_menu::{ContextMenu, ContextMenuItem};
  3use gpui::{
  4    elements::*, platform::CursorStyle, platform::MouseButton, Action, AnyViewHandle, AppContext,
  5    Axis, Entity, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
  6};
  7use schemars::JsonSchema;
  8use serde::{Deserialize, Serialize};
  9use std::rc::Rc;
 10use theme::ThemeSettings;
 11
 12pub trait Panel: View {
 13    fn position(&self, cx: &WindowContext) -> DockPosition;
 14    fn position_is_valid(&self, position: DockPosition) -> bool;
 15    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>);
 16    fn size(&self, cx: &WindowContext) -> f32;
 17    fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>);
 18    fn icon_path(&self, cx: &WindowContext) -> Option<&'static str>;
 19    fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>);
 20    fn icon_label(&self, _: &WindowContext) -> Option<String> {
 21        None
 22    }
 23    fn should_change_position_on_event(_: &Self::Event) -> bool;
 24    fn should_zoom_in_on_event(_: &Self::Event) -> bool {
 25        false
 26    }
 27    fn should_zoom_out_on_event(_: &Self::Event) -> bool {
 28        false
 29    }
 30    fn is_zoomed(&self, _cx: &WindowContext) -> bool {
 31        false
 32    }
 33    fn set_zoomed(&mut self, _zoomed: bool, _cx: &mut ViewContext<Self>) {}
 34    fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {}
 35    fn should_activate_on_event(_: &Self::Event) -> bool {
 36        false
 37    }
 38    fn should_close_on_event(_: &Self::Event) -> bool {
 39        false
 40    }
 41    fn has_focus(&self, cx: &WindowContext) -> bool;
 42    fn is_focus_event(_: &Self::Event) -> bool;
 43}
 44
 45pub trait PanelHandle {
 46    fn id(&self) -> usize;
 47    fn position(&self, cx: &WindowContext) -> DockPosition;
 48    fn position_is_valid(&self, position: DockPosition, cx: &WindowContext) -> bool;
 49    fn set_position(&self, position: DockPosition, cx: &mut WindowContext);
 50    fn is_zoomed(&self, cx: &WindowContext) -> bool;
 51    fn set_zoomed(&self, zoomed: bool, cx: &mut WindowContext);
 52    fn set_active(&self, active: bool, cx: &mut WindowContext);
 53    fn size(&self, cx: &WindowContext) -> f32;
 54    fn set_size(&self, size: Option<f32>, cx: &mut WindowContext);
 55    fn icon_path(&self, cx: &WindowContext) -> Option<&'static str>;
 56    fn icon_tooltip(&self, cx: &WindowContext) -> (String, Option<Box<dyn Action>>);
 57    fn icon_label(&self, cx: &WindowContext) -> Option<String>;
 58    fn has_focus(&self, cx: &WindowContext) -> bool;
 59    fn as_any(&self) -> &AnyViewHandle;
 60}
 61
 62impl<T> PanelHandle for ViewHandle<T>
 63where
 64    T: Panel,
 65{
 66    fn id(&self) -> usize {
 67        self.id()
 68    }
 69
 70    fn position(&self, cx: &WindowContext) -> DockPosition {
 71        self.read(cx).position(cx)
 72    }
 73
 74    fn position_is_valid(&self, position: DockPosition, cx: &WindowContext) -> bool {
 75        self.read(cx).position_is_valid(position)
 76    }
 77
 78    fn set_position(&self, position: DockPosition, cx: &mut WindowContext) {
 79        self.update(cx, |this, cx| this.set_position(position, cx))
 80    }
 81
 82    fn size(&self, cx: &WindowContext) -> f32 {
 83        self.read(cx).size(cx)
 84    }
 85
 86    fn set_size(&self, size: Option<f32>, cx: &mut WindowContext) {
 87        self.update(cx, |this, cx| this.set_size(size, cx))
 88    }
 89
 90    fn is_zoomed(&self, cx: &WindowContext) -> bool {
 91        self.read(cx).is_zoomed(cx)
 92    }
 93
 94    fn set_zoomed(&self, zoomed: bool, cx: &mut WindowContext) {
 95        self.update(cx, |this, cx| this.set_zoomed(zoomed, cx))
 96    }
 97
 98    fn set_active(&self, active: bool, cx: &mut WindowContext) {
 99        self.update(cx, |this, cx| this.set_active(active, cx))
100    }
101
102    fn icon_path(&self, cx: &WindowContext) -> Option<&'static str> {
103        self.read(cx).icon_path(cx)
104    }
105
106    fn icon_tooltip(&self, cx: &WindowContext) -> (String, Option<Box<dyn Action>>) {
107        self.read(cx).icon_tooltip()
108    }
109
110    fn icon_label(&self, cx: &WindowContext) -> Option<String> {
111        self.read(cx).icon_label(cx)
112    }
113
114    fn has_focus(&self, cx: &WindowContext) -> bool {
115        self.read(cx).has_focus(cx)
116    }
117
118    fn as_any(&self) -> &AnyViewHandle {
119        self
120    }
121}
122
123impl From<&dyn PanelHandle> for AnyViewHandle {
124    fn from(val: &dyn PanelHandle) -> Self {
125        val.as_any().clone()
126    }
127}
128
129pub struct Dock {
130    position: DockPosition,
131    panel_entries: Vec<PanelEntry>,
132    is_open: bool,
133    active_panel_index: usize,
134}
135
136#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
137#[serde(rename_all = "lowercase")]
138pub enum DockPosition {
139    Left,
140    Bottom,
141    Right,
142}
143
144impl DockPosition {
145    fn to_label(&self) -> &'static str {
146        match self {
147            Self::Left => "left",
148            Self::Bottom => "bottom",
149            Self::Right => "right",
150        }
151    }
152
153    fn to_resize_handle_side(self) -> HandleSide {
154        match self {
155            Self::Left => HandleSide::Right,
156            Self::Bottom => HandleSide::Top,
157            Self::Right => HandleSide::Left,
158        }
159    }
160
161    pub fn axis(&self) -> Axis {
162        match self {
163            Self::Left | Self::Right => Axis::Horizontal,
164            Self::Bottom => Axis::Vertical,
165        }
166    }
167}
168
169struct PanelEntry {
170    panel: Rc<dyn PanelHandle>,
171    context_menu: ViewHandle<ContextMenu>,
172    _subscriptions: [Subscription; 2],
173}
174
175pub struct PanelButtons {
176    dock: ViewHandle<Dock>,
177    workspace: WeakViewHandle<Workspace>,
178}
179
180impl Dock {
181    pub fn new(position: DockPosition) -> Self {
182        Self {
183            position,
184            panel_entries: Default::default(),
185            active_panel_index: 0,
186            is_open: false,
187        }
188    }
189
190    pub fn position(&self) -> DockPosition {
191        self.position
192    }
193
194    pub fn is_open(&self) -> bool {
195        self.is_open
196    }
197
198    pub fn has_focus(&self, cx: &WindowContext) -> bool {
199        self.visible_panel()
200            .map_or(false, |panel| panel.has_focus(cx))
201    }
202
203    pub fn panel<T: Panel>(&self) -> Option<ViewHandle<T>> {
204        self.panel_entries
205            .iter()
206            .find_map(|entry| entry.panel.as_any().clone().downcast())
207    }
208
209    pub fn panel_index_for_type<T: Panel>(&self) -> Option<usize> {
210        self.panel_entries
211            .iter()
212            .position(|entry| entry.panel.as_any().is::<T>())
213    }
214
215    pub fn panel_index_for_ui_name(&self, ui_name: &str, cx: &AppContext) -> Option<usize> {
216        self.panel_entries.iter().position(|entry| {
217            let panel = entry.panel.as_any();
218            cx.view_ui_name(panel.window(), panel.id()) == Some(ui_name)
219        })
220    }
221
222    pub fn active_panel_index(&self) -> usize {
223        self.active_panel_index
224    }
225
226    pub(crate) fn set_open(&mut self, open: bool, cx: &mut ViewContext<Self>) {
227        if open != self.is_open {
228            self.is_open = open;
229            if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) {
230                active_panel.panel.set_active(open, cx);
231            }
232
233            cx.notify();
234        }
235    }
236
237    pub fn set_panel_zoomed(
238        &mut self,
239        panel: &AnyViewHandle,
240        zoomed: bool,
241        cx: &mut ViewContext<Self>,
242    ) {
243        for entry in &mut self.panel_entries {
244            if entry.panel.as_any() == panel {
245                if zoomed != entry.panel.is_zoomed(cx) {
246                    entry.panel.set_zoomed(zoomed, cx);
247                }
248            } else if entry.panel.is_zoomed(cx) {
249                entry.panel.set_zoomed(false, cx);
250            }
251        }
252
253        cx.notify();
254    }
255
256    pub fn zoom_out(&mut self, cx: &mut ViewContext<Self>) {
257        for entry in &mut self.panel_entries {
258            if entry.panel.is_zoomed(cx) {
259                entry.panel.set_zoomed(false, cx);
260            }
261        }
262    }
263
264    pub(crate) fn add_panel<T: Panel>(&mut self, panel: ViewHandle<T>, cx: &mut ViewContext<Self>) {
265        let subscriptions = [
266            cx.observe(&panel, |_, _, cx| cx.notify()),
267            cx.subscribe(&panel, |this, panel, event, cx| {
268                if T::should_activate_on_event(event) {
269                    if let Some(ix) = this
270                        .panel_entries
271                        .iter()
272                        .position(|entry| entry.panel.id() == panel.id())
273                    {
274                        this.set_open(true, cx);
275                        this.activate_panel(ix, cx);
276                        cx.focus(&panel);
277                    }
278                } else if T::should_close_on_event(event)
279                    && this.visible_panel().map_or(false, |p| p.id() == panel.id())
280                {
281                    this.set_open(false, cx);
282                }
283            }),
284        ];
285
286        let dock_view_id = cx.view_id();
287        self.panel_entries.push(PanelEntry {
288            panel: Rc::new(panel),
289            context_menu: cx.add_view(|cx| {
290                let mut menu = ContextMenu::new(dock_view_id, cx);
291                menu.set_position_mode(OverlayPositionMode::Local);
292                menu
293            }),
294            _subscriptions: subscriptions,
295        });
296        cx.notify()
297    }
298
299    pub fn remove_panel<T: Panel>(&mut self, panel: &ViewHandle<T>, cx: &mut ViewContext<Self>) {
300        if let Some(panel_ix) = self
301            .panel_entries
302            .iter()
303            .position(|entry| entry.panel.id() == panel.id())
304        {
305            if panel_ix == self.active_panel_index {
306                self.active_panel_index = 0;
307                self.set_open(false, cx);
308            } else if panel_ix < self.active_panel_index {
309                self.active_panel_index -= 1;
310            }
311            self.panel_entries.remove(panel_ix);
312            cx.notify();
313        }
314    }
315
316    pub fn panels_len(&self) -> usize {
317        self.panel_entries.len()
318    }
319
320    pub fn activate_panel(&mut self, panel_ix: usize, cx: &mut ViewContext<Self>) {
321        if panel_ix != self.active_panel_index {
322            if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) {
323                active_panel.panel.set_active(false, cx);
324            }
325
326            self.active_panel_index = panel_ix;
327            if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) {
328                active_panel.panel.set_active(true, cx);
329            }
330
331            cx.notify();
332        }
333    }
334
335    pub fn visible_panel(&self) -> Option<&Rc<dyn PanelHandle>> {
336        let entry = self.visible_entry()?;
337        Some(&entry.panel)
338    }
339
340    pub fn active_panel(&self) -> Option<&Rc<dyn PanelHandle>> {
341        Some(&self.panel_entries.get(self.active_panel_index)?.panel)
342    }
343
344    fn visible_entry(&self) -> Option<&PanelEntry> {
345        if self.is_open {
346            self.panel_entries.get(self.active_panel_index)
347        } else {
348            None
349        }
350    }
351
352    pub fn zoomed_panel(&self, cx: &WindowContext) -> Option<Rc<dyn PanelHandle>> {
353        let entry = self.visible_entry()?;
354        if entry.panel.is_zoomed(cx) {
355            Some(entry.panel.clone())
356        } else {
357            None
358        }
359    }
360
361    pub fn panel_size(&self, panel: &dyn PanelHandle, cx: &WindowContext) -> Option<f32> {
362        self.panel_entries
363            .iter()
364            .find(|entry| entry.panel.id() == panel.id())
365            .map(|entry| entry.panel.size(cx))
366    }
367
368    pub fn active_panel_size(&self, cx: &WindowContext) -> Option<f32> {
369        if self.is_open {
370            self.panel_entries
371                .get(self.active_panel_index)
372                .map(|entry| entry.panel.size(cx))
373        } else {
374            None
375        }
376    }
377
378    pub fn resize_active_panel(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
379        if let Some(entry) = self.panel_entries.get_mut(self.active_panel_index) {
380            entry.panel.set_size(size, cx);
381            cx.notify();
382        }
383    }
384
385    pub fn render_placeholder(&self, cx: &WindowContext) -> AnyElement<Workspace> {
386        if let Some(active_entry) = self.visible_entry() {
387            Empty::new()
388                .into_any()
389                .contained()
390                .with_style(self.style(cx))
391                .resizable::<WorkspaceBounds>(
392                    self.position.to_resize_handle_side(),
393                    active_entry.panel.size(cx),
394                    |_, _, _| {},
395                )
396                .into_any()
397        } else {
398            Empty::new().into_any()
399        }
400    }
401
402    fn style(&self, cx: &WindowContext) -> ContainerStyle {
403        let theme = &settings::get::<ThemeSettings>(cx).theme;
404        let style = match self.position {
405            DockPosition::Left => theme.workspace.dock.left,
406            DockPosition::Bottom => theme.workspace.dock.bottom,
407            DockPosition::Right => theme.workspace.dock.right,
408        };
409        style
410    }
411}
412
413impl Entity for Dock {
414    type Event = ();
415}
416
417impl View for Dock {
418    fn ui_name() -> &'static str {
419        "Dock"
420    }
421
422    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
423        if let Some(active_entry) = self.visible_entry() {
424            let style = self.style(cx);
425            ChildView::new(active_entry.panel.as_any(), cx)
426                .contained()
427                .with_style(style)
428                .resizable::<WorkspaceBounds>(
429                    self.position.to_resize_handle_side(),
430                    active_entry.panel.size(cx),
431                    |dock: &mut Self, size, cx| dock.resize_active_panel(size, cx),
432                )
433                .into_any()
434        } else {
435            Empty::new().into_any()
436        }
437    }
438
439    fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
440        if cx.is_self_focused() {
441            if let Some(active_entry) = self.visible_entry() {
442                cx.focus(active_entry.panel.as_any());
443            } else {
444                cx.focus_parent();
445            }
446        }
447    }
448}
449
450impl PanelButtons {
451    pub fn new(
452        dock: ViewHandle<Dock>,
453        workspace: WeakViewHandle<Workspace>,
454        cx: &mut ViewContext<Self>,
455    ) -> Self {
456        cx.observe(&dock, |_, _, cx| cx.notify()).detach();
457        Self { dock, workspace }
458    }
459}
460
461impl Entity for PanelButtons {
462    type Event = ();
463}
464
465impl View for PanelButtons {
466    fn ui_name() -> &'static str {
467        "PanelButtons"
468    }
469
470    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
471        let theme = &settings::get::<ThemeSettings>(cx).theme;
472        let tooltip_style = theme.tooltip.clone();
473        let theme = &theme.workspace.status_bar.panel_buttons;
474        let button_style = theme.button.clone();
475        let dock = self.dock.read(cx);
476        let active_ix = dock.active_panel_index;
477        let is_open = dock.is_open;
478        let dock_position = dock.position;
479        let group_style = match dock_position {
480            DockPosition::Left => theme.group_left,
481            DockPosition::Bottom => theme.group_bottom,
482            DockPosition::Right => theme.group_right,
483        };
484        let menu_corner = match dock_position {
485            DockPosition::Left => AnchorCorner::BottomLeft,
486            DockPosition::Bottom | DockPosition::Right => AnchorCorner::BottomRight,
487        };
488
489        let panels = dock
490            .panel_entries
491            .iter()
492            .map(|item| (item.panel.clone(), item.context_menu.clone()))
493            .collect::<Vec<_>>();
494        Flex::row()
495            .with_children(panels.into_iter().enumerate().filter_map(
496                |(panel_ix, (view, context_menu))| {
497                    let icon_path = view.icon_path(cx)?;
498                    let is_active = is_open && panel_ix == active_ix;
499                    let (tooltip, tooltip_action) = if is_active {
500                        (
501                            format!("Close {} dock", dock_position.to_label()),
502                            Some(match dock_position {
503                                DockPosition::Left => crate::ToggleLeftDock.boxed_clone(),
504                                DockPosition::Bottom => crate::ToggleBottomDock.boxed_clone(),
505                                DockPosition::Right => crate::ToggleRightDock.boxed_clone(),
506                            }),
507                        )
508                    } else {
509                        view.icon_tooltip(cx)
510                    };
511                    Some(
512                        Stack::new()
513                            .with_child(
514                                MouseEventHandler::new::<Self, _>(panel_ix, cx, |state, cx| {
515                                    let style = button_style.in_state(is_active);
516
517                                    let style = style.style_for(state);
518                                    Flex::row()
519                                        .with_child(
520                                            Svg::new(icon_path)
521                                                .with_color(style.icon_color)
522                                                .constrained()
523                                                .with_width(style.icon_size)
524                                                .aligned(),
525                                        )
526                                        .with_children(if let Some(label) = view.icon_label(cx) {
527                                            Some(
528                                                Label::new(label, style.label.text.clone())
529                                                    .contained()
530                                                    .with_style(style.label.container)
531                                                    .aligned(),
532                                            )
533                                        } else {
534                                            None
535                                        })
536                                        .constrained()
537                                        .with_height(style.icon_size)
538                                        .contained()
539                                        .with_style(style.container)
540                                })
541                                .with_cursor_style(CursorStyle::PointingHand)
542                                .on_click(MouseButton::Left, {
543                                    let tooltip_action =
544                                        tooltip_action.as_ref().map(|action| action.boxed_clone());
545                                    move |_, this, cx| {
546                                        if let Some(tooltip_action) = &tooltip_action {
547                                            let window = cx.window();
548                                            let view_id = this.workspace.id();
549                                            let tooltip_action = tooltip_action.boxed_clone();
550                                            cx.spawn(|_, mut cx| async move {
551                                                window.dispatch_action(
552                                                    view_id,
553                                                    &*tooltip_action,
554                                                    &mut cx,
555                                                );
556                                            })
557                                            .detach();
558                                        }
559                                    }
560                                })
561                                .on_click(MouseButton::Right, {
562                                    let view = view.clone();
563                                    let menu = context_menu.clone();
564                                    move |_, _, cx| {
565                                        const POSITIONS: [DockPosition; 3] = [
566                                            DockPosition::Left,
567                                            DockPosition::Right,
568                                            DockPosition::Bottom,
569                                        ];
570
571                                        menu.update(cx, |menu, cx| {
572                                            let items = POSITIONS
573                                                .into_iter()
574                                                .filter(|position| {
575                                                    *position != dock_position
576                                                        && view.position_is_valid(*position, cx)
577                                                })
578                                                .map(|position| {
579                                                    let view = view.clone();
580                                                    ContextMenuItem::handler(
581                                                        format!("Dock {}", position.to_label()),
582                                                        move |cx| view.set_position(position, cx),
583                                                    )
584                                                })
585                                                .collect();
586                                            menu.show(Default::default(), menu_corner, items, cx);
587                                        })
588                                    }
589                                })
590                                .with_tooltip::<Self>(
591                                    panel_ix,
592                                    tooltip,
593                                    tooltip_action,
594                                    tooltip_style.clone(),
595                                    cx,
596                                ),
597                            )
598                            .with_child(ChildView::new(&context_menu, cx)),
599                    )
600                },
601            ))
602            .contained()
603            .with_style(group_style)
604            .into_any()
605    }
606}
607
608impl StatusItemView for PanelButtons {
609    fn set_active_pane_item(
610        &mut self,
611        _: Option<&dyn crate::ItemHandle>,
612        _: &mut ViewContext<Self>,
613    ) {
614    }
615}
616
617#[cfg(any(test, feature = "test-support"))]
618pub mod test {
619    use super::*;
620    use gpui::{ViewContext, WindowContext};
621
622    #[derive(Debug)]
623    pub enum TestPanelEvent {
624        PositionChanged,
625        Activated,
626        Closed,
627        ZoomIn,
628        ZoomOut,
629        Focus,
630    }
631
632    pub struct TestPanel {
633        pub position: DockPosition,
634        pub zoomed: bool,
635        pub active: bool,
636        pub has_focus: bool,
637        pub size: f32,
638    }
639
640    impl TestPanel {
641        pub fn new(position: DockPosition) -> Self {
642            Self {
643                position,
644                zoomed: false,
645                active: false,
646                has_focus: false,
647                size: 300.,
648            }
649        }
650    }
651
652    impl Entity for TestPanel {
653        type Event = TestPanelEvent;
654    }
655
656    impl View for TestPanel {
657        fn ui_name() -> &'static str {
658            "TestPanel"
659        }
660
661        fn render(&mut self, _: &mut ViewContext<'_, '_, Self>) -> AnyElement<Self> {
662            Empty::new().into_any()
663        }
664
665        fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
666            self.has_focus = true;
667            cx.emit(TestPanelEvent::Focus);
668        }
669
670        fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
671            self.has_focus = false;
672        }
673    }
674
675    impl Panel for TestPanel {
676        fn position(&self, _: &gpui::WindowContext) -> super::DockPosition {
677            self.position
678        }
679
680        fn position_is_valid(&self, _: super::DockPosition) -> bool {
681            true
682        }
683
684        fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
685            self.position = position;
686            cx.emit(TestPanelEvent::PositionChanged);
687        }
688
689        fn is_zoomed(&self, _: &WindowContext) -> bool {
690            self.zoomed
691        }
692
693        fn set_zoomed(&mut self, zoomed: bool, _cx: &mut ViewContext<Self>) {
694            self.zoomed = zoomed;
695        }
696
697        fn set_active(&mut self, active: bool, _cx: &mut ViewContext<Self>) {
698            self.active = active;
699        }
700
701        fn size(&self, _: &WindowContext) -> f32 {
702            self.size
703        }
704
705        fn set_size(&mut self, size: Option<f32>, _: &mut ViewContext<Self>) {
706            self.size = size.unwrap_or(300.);
707        }
708
709        fn icon_path(&self, _: &WindowContext) -> Option<&'static str> {
710            Some("icons/test_panel.svg")
711        }
712
713        fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {
714            ("Test Panel".into(), None)
715        }
716
717        fn should_change_position_on_event(event: &Self::Event) -> bool {
718            matches!(event, TestPanelEvent::PositionChanged)
719        }
720
721        fn should_zoom_in_on_event(event: &Self::Event) -> bool {
722            matches!(event, TestPanelEvent::ZoomIn)
723        }
724
725        fn should_zoom_out_on_event(event: &Self::Event) -> bool {
726            matches!(event, TestPanelEvent::ZoomOut)
727        }
728
729        fn should_activate_on_event(event: &Self::Event) -> bool {
730            matches!(event, TestPanelEvent::Activated)
731        }
732
733        fn should_close_on_event(event: &Self::Event) -> bool {
734            matches!(event, TestPanelEvent::Closed)
735        }
736
737        fn has_focus(&self, _cx: &WindowContext) -> bool {
738            self.has_focus
739        }
740
741        fn is_focus_event(event: &Self::Event) -> bool {
742            matches!(event, TestPanelEvent::Focus)
743        }
744    }
745}