dock.rs

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