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