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