dock.rs

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