dock.rs

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