dock.rs

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