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