dock.rs

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