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