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.remove_panel(&panel, cx);
377                        new_dock.add_panel(panel.clone(), workspace.clone(), cx);
378                        if was_visible {
379                            new_dock.set_open(true, cx);
380                            new_dock.activate_panel(new_dock.panels_len() - 1, cx);
381                        }
382                    });
383                }
384            }),
385            cx.subscribe(&panel, move |this, panel, event, cx| match event {
386                PanelEvent::ZoomIn => {
387                    this.set_panel_zoomed(&panel.to_any(), true, cx);
388                    if !panel.focus_handle(cx).contains_focused(cx) {
389                        cx.focus_view(&panel);
390                    }
391                    workspace
392                        .update(cx, |workspace, cx| {
393                            workspace.zoomed = Some(panel.downgrade().into());
394                            workspace.zoomed_position = Some(panel.read(cx).position(cx));
395                            cx.emit(Event::ZoomChanged);
396                        })
397                        .ok();
398                }
399                PanelEvent::ZoomOut => {
400                    this.set_panel_zoomed(&panel.to_any(), false, cx);
401                    workspace
402                        .update(cx, |workspace, cx| {
403                            if workspace.zoomed_position == Some(this.position) {
404                                workspace.zoomed = None;
405                                workspace.zoomed_position = None;
406                                cx.emit(Event::ZoomChanged);
407                            }
408                            cx.notify();
409                        })
410                        .ok();
411                }
412                PanelEvent::Activate => {
413                    if let Some(ix) = this
414                        .panel_entries
415                        .iter()
416                        .position(|entry| entry.panel.panel_id() == Entity::entity_id(&panel))
417                    {
418                        this.set_open(true, cx);
419                        this.activate_panel(ix, cx);
420                        cx.focus_view(&panel);
421                    }
422                }
423                PanelEvent::Close => {
424                    if this
425                        .visible_panel()
426                        .map_or(false, |p| p.panel_id() == Entity::entity_id(&panel))
427                    {
428                        this.set_open(false, cx);
429                    }
430                }
431            }),
432        ];
433
434        self.panel_entries.push(PanelEntry {
435            panel: Arc::new(panel.clone()),
436            _subscriptions: subscriptions,
437        });
438
439        if !self.restore_state(cx) && panel.read(cx).starts_open(cx) {
440            self.activate_panel(self.panel_entries.len() - 1, cx);
441            self.set_open(true, cx);
442        }
443
444        cx.notify()
445    }
446
447    pub fn restore_state(&mut self, cx: &mut ViewContext<Self>) -> bool {
448        if let Some(serialized) = self.serialized_dock.clone() {
449            if let Some(active_panel) = serialized.active_panel {
450                if let Some(idx) = self.panel_index_for_persistent_name(active_panel.as_str(), cx) {
451                    self.activate_panel(idx, cx);
452                }
453            }
454
455            if serialized.zoom {
456                if let Some(panel) = self.active_panel() {
457                    panel.set_zoomed(true, cx)
458                }
459            }
460            self.set_open(serialized.visible, cx);
461            return true;
462        }
463        return false;
464    }
465
466    pub fn remove_panel<T: Panel>(&mut self, panel: &View<T>, cx: &mut ViewContext<Self>) {
467        if let Some(panel_ix) = self
468            .panel_entries
469            .iter()
470            .position(|entry| entry.panel.panel_id() == Entity::entity_id(panel))
471        {
472            if panel_ix == self.active_panel_index {
473                self.active_panel_index = 0;
474                self.set_open(false, cx);
475            } else if panel_ix < self.active_panel_index {
476                self.active_panel_index -= 1;
477            }
478            self.panel_entries.remove(panel_ix);
479            cx.notify();
480        }
481    }
482
483    pub fn panels_len(&self) -> usize {
484        self.panel_entries.len()
485    }
486
487    pub fn activate_panel(&mut self, panel_ix: usize, cx: &mut ViewContext<Self>) {
488        if panel_ix != self.active_panel_index {
489            if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) {
490                active_panel.panel.set_active(false, cx);
491            }
492
493            self.active_panel_index = panel_ix;
494            if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) {
495                active_panel.panel.set_active(true, cx);
496            }
497
498            cx.notify();
499        }
500    }
501
502    pub fn visible_panel(&self) -> Option<&Arc<dyn PanelHandle>> {
503        let entry = self.visible_entry()?;
504        Some(&entry.panel)
505    }
506
507    pub fn active_panel(&self) -> Option<&Arc<dyn PanelHandle>> {
508        Some(&self.panel_entries.get(self.active_panel_index)?.panel)
509    }
510
511    fn visible_entry(&self) -> Option<&PanelEntry> {
512        if self.is_open {
513            self.panel_entries.get(self.active_panel_index)
514        } else {
515            None
516        }
517    }
518
519    pub fn zoomed_panel(&self, cx: &WindowContext) -> Option<Arc<dyn PanelHandle>> {
520        let entry = self.visible_entry()?;
521        if entry.panel.is_zoomed(cx) {
522            Some(entry.panel.clone())
523        } else {
524            None
525        }
526    }
527
528    pub fn panel_size(&self, panel: &dyn PanelHandle, cx: &WindowContext) -> Option<Pixels> {
529        self.panel_entries
530            .iter()
531            .find(|entry| entry.panel.panel_id() == panel.panel_id())
532            .map(|entry| entry.panel.size(cx))
533    }
534
535    pub fn active_panel_size(&self, cx: &WindowContext) -> Option<Pixels> {
536        if self.is_open {
537            self.panel_entries
538                .get(self.active_panel_index)
539                .map(|entry| entry.panel.size(cx))
540        } else {
541            None
542        }
543    }
544
545    pub fn resize_active_panel(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
546        if let Some(entry) = self.panel_entries.get_mut(self.active_panel_index) {
547            let size = size.map(|size| size.max(RESIZE_HANDLE_SIZE).round());
548            entry.panel.set_size(size, cx);
549            cx.notify();
550        }
551    }
552
553    pub fn toggle_action(&self) -> Box<dyn Action> {
554        match self.position {
555            DockPosition::Left => crate::ToggleLeftDock.boxed_clone(),
556            DockPosition::Bottom => crate::ToggleBottomDock.boxed_clone(),
557            DockPosition::Right => crate::ToggleRightDock.boxed_clone(),
558        }
559    }
560
561    fn dispatch_context() -> KeyContext {
562        let mut dispatch_context = KeyContext::new_with_defaults();
563        dispatch_context.add("Dock");
564
565        dispatch_context
566    }
567}
568
569impl Render for Dock {
570    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
571        let dispatch_context = Self::dispatch_context();
572        if let Some(entry) = self.visible_entry() {
573            let size = entry.panel.size(cx);
574
575            let position = self.position;
576            let create_resize_handle = || {
577                let handle = div()
578                    .id("resize-handle")
579                    .on_drag(DraggedDock(position), |dock, cx| {
580                        cx.stop_propagation();
581                        cx.new_view(|_| dock.clone())
582                    })
583                    .on_mouse_down(
584                        MouseButton::Left,
585                        cx.listener(|_, _: &MouseDownEvent, cx| {
586                            cx.stop_propagation();
587                        }),
588                    )
589                    .on_mouse_up(
590                        MouseButton::Left,
591                        cx.listener(|v, e: &MouseUpEvent, cx| {
592                            if e.click_count == 2 {
593                                v.resize_active_panel(None, cx);
594                                cx.stop_propagation();
595                            }
596                        }),
597                    )
598                    .occlude();
599                match self.position() {
600                    DockPosition::Left => deferred(
601                        handle
602                            .absolute()
603                            .right(-RESIZE_HANDLE_SIZE / 2.)
604                            .top(px(0.))
605                            .h_full()
606                            .w(RESIZE_HANDLE_SIZE)
607                            .cursor_col_resize(),
608                    ),
609                    DockPosition::Bottom => deferred(
610                        handle
611                            .absolute()
612                            .top(-RESIZE_HANDLE_SIZE / 2.)
613                            .left(px(0.))
614                            .w_full()
615                            .h(RESIZE_HANDLE_SIZE)
616                            .cursor_row_resize(),
617                    ),
618                    DockPosition::Right => deferred(
619                        handle
620                            .absolute()
621                            .top(px(0.))
622                            .left(-RESIZE_HANDLE_SIZE / 2.)
623                            .h_full()
624                            .w(RESIZE_HANDLE_SIZE)
625                            .cursor_col_resize(),
626                    ),
627                }
628            };
629
630            div()
631                .key_context(dispatch_context)
632                .track_focus(&self.focus_handle)
633                .flex()
634                .bg(cx.theme().colors().panel_background)
635                .border_color(cx.theme().colors().border)
636                .overflow_hidden()
637                .map(|this| match self.position().axis() {
638                    Axis::Horizontal => this.w(size).h_full().flex_row(),
639                    Axis::Vertical => this.h(size).w_full().flex_col(),
640                })
641                .map(|this| match self.position() {
642                    DockPosition::Left => this.border_r_1(),
643                    DockPosition::Right => this.border_l_1(),
644                    DockPosition::Bottom => this.border_t_1(),
645                })
646                .child(
647                    div()
648                        .map(|this| match self.position().axis() {
649                            Axis::Horizontal => this.min_w(size).h_full(),
650                            Axis::Vertical => this.min_h(size).w_full(),
651                        })
652                        .child(
653                            entry
654                                .panel
655                                .to_any()
656                                .cached(StyleRefinement::default().v_flex().size_full()),
657                        ),
658                )
659                .when(self.resizeable, |this| this.child(create_resize_handle()))
660        } else {
661            div()
662                .key_context(dispatch_context)
663                .track_focus(&self.focus_handle)
664        }
665    }
666}
667
668impl PanelButtons {
669    pub fn new(dock: View<Dock>, cx: &mut ViewContext<Self>) -> Self {
670        cx.observe(&dock, |_, _, cx| cx.notify()).detach();
671        Self { dock }
672    }
673}
674
675impl Render for PanelButtons {
676    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
677        let dock = self.dock.read(cx);
678        let active_index = dock.active_panel_index;
679        let is_open = dock.is_open;
680        let dock_position = dock.position;
681
682        let (menu_anchor, menu_attach) = match dock.position {
683            DockPosition::Left => (AnchorCorner::BottomLeft, AnchorCorner::TopLeft),
684            DockPosition::Bottom | DockPosition::Right => {
685                (AnchorCorner::BottomRight, AnchorCorner::TopRight)
686            }
687        };
688
689        let buttons = dock
690            .panel_entries
691            .iter()
692            .enumerate()
693            .filter_map(|(i, entry)| {
694                let icon = entry.panel.icon(cx)?;
695                let icon_tooltip = entry.panel.icon_tooltip(cx)?;
696                let name = entry.panel.persistent_name();
697                let panel = entry.panel.clone();
698
699                let is_active_button = i == active_index && is_open;
700                let (action, tooltip) = if is_active_button {
701                    let action = dock.toggle_action();
702
703                    let tooltip: SharedString =
704                        format!("Close {} dock", dock.position.to_label()).into();
705
706                    (action, tooltip)
707                } else {
708                    let action = entry.panel.toggle_action(cx);
709
710                    (action, icon_tooltip.into())
711                };
712
713                Some(
714                    right_click_menu(name)
715                        .menu(move |cx| {
716                            const POSITIONS: [DockPosition; 3] = [
717                                DockPosition::Left,
718                                DockPosition::Right,
719                                DockPosition::Bottom,
720                            ];
721
722                            ContextMenu::build(cx, |mut menu, cx| {
723                                for position in POSITIONS {
724                                    if position != dock_position
725                                        && panel.position_is_valid(position, cx)
726                                    {
727                                        let panel = panel.clone();
728                                        menu = menu.entry(
729                                            format!("Dock {}", position.to_label()),
730                                            None,
731                                            move |cx| {
732                                                panel.set_position(position, cx);
733                                            },
734                                        )
735                                    }
736                                }
737                                menu
738                            })
739                        })
740                        .anchor(menu_anchor)
741                        .attach(menu_attach)
742                        .trigger(
743                            IconButton::new(name, icon)
744                                .icon_size(IconSize::Small)
745                                .selected(is_active_button)
746                                .on_click({
747                                    let action = action.boxed_clone();
748                                    move |_, cx| cx.dispatch_action(action.boxed_clone())
749                                })
750                                .tooltip(move |cx| {
751                                    Tooltip::for_action(tooltip.clone(), &*action, cx)
752                                }),
753                        ),
754                )
755            });
756
757        h_flex().gap_0p5().children(buttons)
758    }
759}
760
761impl StatusItemView for PanelButtons {
762    fn set_active_pane_item(
763        &mut self,
764        _active_pane_item: Option<&dyn crate::ItemHandle>,
765        _cx: &mut ViewContext<Self>,
766    ) {
767        // Nothing to do, panel buttons don't depend on the active center item
768    }
769}
770
771#[cfg(any(test, feature = "test-support"))]
772pub mod test {
773    use super::*;
774    use gpui::{actions, div, ViewContext, WindowContext};
775
776    pub struct TestPanel {
777        pub position: DockPosition,
778        pub zoomed: bool,
779        pub active: bool,
780        pub focus_handle: FocusHandle,
781        pub size: Pixels,
782    }
783    actions!(test, [ToggleTestPanel]);
784
785    impl EventEmitter<PanelEvent> for TestPanel {}
786
787    impl TestPanel {
788        pub fn new(position: DockPosition, cx: &mut WindowContext) -> Self {
789            Self {
790                position,
791                zoomed: false,
792                active: false,
793                focus_handle: cx.focus_handle(),
794                size: px(300.),
795            }
796        }
797    }
798
799    impl Render for TestPanel {
800        fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
801            div().id("test").track_focus(&self.focus_handle)
802        }
803    }
804
805    impl Panel for TestPanel {
806        fn persistent_name() -> &'static str {
807            "TestPanel"
808        }
809
810        fn position(&self, _: &gpui::WindowContext) -> super::DockPosition {
811            self.position
812        }
813
814        fn position_is_valid(&self, _: super::DockPosition) -> bool {
815            true
816        }
817
818        fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
819            self.position = position;
820            cx.update_global::<SettingsStore, _>(|_, _| {});
821        }
822
823        fn size(&self, _: &WindowContext) -> Pixels {
824            self.size
825        }
826
827        fn set_size(&mut self, size: Option<Pixels>, _: &mut ViewContext<Self>) {
828            self.size = size.unwrap_or(px(300.));
829        }
830
831        fn icon(&self, _: &WindowContext) -> Option<ui::IconName> {
832            None
833        }
834
835        fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
836            None
837        }
838
839        fn toggle_action(&self) -> Box<dyn Action> {
840            ToggleTestPanel.boxed_clone()
841        }
842
843        fn is_zoomed(&self, _: &WindowContext) -> bool {
844            self.zoomed
845        }
846
847        fn set_zoomed(&mut self, zoomed: bool, _cx: &mut ViewContext<Self>) {
848            self.zoomed = zoomed;
849        }
850
851        fn set_active(&mut self, active: bool, _cx: &mut ViewContext<Self>) {
852            self.active = active;
853        }
854    }
855
856    impl FocusableView for TestPanel {
857        fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
858            self.focus_handle.clone()
859        }
860    }
861}