dock.rs

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