dock.rs

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