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, ClickEvent, Entity,
  6    EntityId, EventEmitter, FocusHandle, FocusableView, IntoElement, KeyContext, MouseButton,
  7    ParentElement, Render, SharedString, StyleRefinement, Styled, Subscription, View, ViewContext,
  8    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::default();
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_click(cx.listener(|v, e: &ClickEvent, cx| {
577                        if e.down.button == MouseButton::Left && e.down.click_count == 2 {
578                            v.resize_active_panel(None, cx);
579                            cx.stop_propagation();
580                        }
581                    }))
582                    .occlude();
583                match self.position() {
584                    DockPosition::Left => deferred(
585                        handle
586                            .absolute()
587                            .right(-RESIZE_HANDLE_SIZE / 2.)
588                            .top(px(0.))
589                            .h_full()
590                            .w(RESIZE_HANDLE_SIZE)
591                            .cursor_col_resize(),
592                    ),
593                    DockPosition::Bottom => deferred(
594                        handle
595                            .absolute()
596                            .top(-RESIZE_HANDLE_SIZE / 2.)
597                            .left(px(0.))
598                            .w_full()
599                            .h(RESIZE_HANDLE_SIZE)
600                            .cursor_row_resize(),
601                    ),
602                    DockPosition::Right => deferred(
603                        handle
604                            .absolute()
605                            .top(px(0.))
606                            .left(-RESIZE_HANDLE_SIZE / 2.)
607                            .h_full()
608                            .w(RESIZE_HANDLE_SIZE)
609                            .cursor_col_resize(),
610                    ),
611                }
612            };
613
614            div()
615                .key_context(dispatch_context)
616                .track_focus(&self.focus_handle)
617                .flex()
618                .bg(cx.theme().colors().panel_background)
619                .border_color(cx.theme().colors().border)
620                .overflow_hidden()
621                .map(|this| match self.position().axis() {
622                    Axis::Horizontal => this.w(size).h_full().flex_row(),
623                    Axis::Vertical => this.h(size).w_full().flex_col(),
624                })
625                .map(|this| match self.position() {
626                    DockPosition::Left => this.border_r(),
627                    DockPosition::Right => this.border_l(),
628                    DockPosition::Bottom => this.border_t(),
629                })
630                .child(
631                    div()
632                        .map(|this| match self.position().axis() {
633                            Axis::Horizontal => this.min_w(size).h_full(),
634                            Axis::Vertical => this.min_h(size).w_full(),
635                        })
636                        .child(
637                            entry
638                                .panel
639                                .to_any()
640                                .cached(StyleRefinement::default().v_flex().size_full()),
641                        ),
642                )
643                .when(self.resizeable, |this| this.child(create_resize_handle()))
644        } else {
645            div()
646                .key_context(dispatch_context)
647                .track_focus(&self.focus_handle)
648        }
649    }
650}
651
652impl PanelButtons {
653    pub fn new(dock: View<Dock>, cx: &mut ViewContext<Self>) -> Self {
654        cx.observe(&dock, |_, _, cx| cx.notify()).detach();
655        Self { dock }
656    }
657}
658
659impl Render for PanelButtons {
660    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
661        let dock = self.dock.read(cx);
662        let active_index = dock.active_panel_index;
663        let is_open = dock.is_open;
664        let dock_position = dock.position;
665
666        let (menu_anchor, menu_attach) = match dock.position {
667            DockPosition::Left => (AnchorCorner::BottomLeft, AnchorCorner::TopLeft),
668            DockPosition::Bottom | DockPosition::Right => {
669                (AnchorCorner::BottomRight, AnchorCorner::TopRight)
670            }
671        };
672
673        let buttons = dock
674            .panel_entries
675            .iter()
676            .enumerate()
677            .filter_map(|(i, entry)| {
678                let icon = entry.panel.icon(cx)?;
679                let icon_tooltip = entry.panel.icon_tooltip(cx)?;
680                let name = entry.panel.persistent_name();
681                let panel = entry.panel.clone();
682
683                let is_active_button = i == active_index && is_open;
684                let (action, tooltip) = if is_active_button {
685                    let action = dock.toggle_action();
686
687                    let tooltip: SharedString =
688                        format!("Close {} dock", dock.position.to_label()).into();
689
690                    (action, tooltip)
691                } else {
692                    let action = entry.panel.toggle_action(cx);
693
694                    (action, icon_tooltip.into())
695                };
696
697                Some(
698                    right_click_menu(name)
699                        .menu(move |cx| {
700                            const POSITIONS: [DockPosition; 3] = [
701                                DockPosition::Left,
702                                DockPosition::Right,
703                                DockPosition::Bottom,
704                            ];
705
706                            ContextMenu::build(cx, |mut menu, cx| {
707                                for position in POSITIONS {
708                                    if position != dock_position
709                                        && panel.position_is_valid(position, cx)
710                                    {
711                                        let panel = panel.clone();
712                                        menu = menu.entry(
713                                            format!("Dock {}", position.to_label()),
714                                            None,
715                                            move |cx| {
716                                                panel.set_position(position, cx);
717                                            },
718                                        )
719                                    }
720                                }
721                                menu
722                            })
723                        })
724                        .anchor(menu_anchor)
725                        .attach(menu_attach)
726                        .trigger(
727                            IconButton::new(name, icon)
728                                .icon_size(IconSize::Small)
729                                .selected(is_active_button)
730                                .on_click({
731                                    let action = action.boxed_clone();
732                                    move |_, cx| cx.dispatch_action(action.boxed_clone())
733                                })
734                                .tooltip(move |cx| {
735                                    Tooltip::for_action(tooltip.clone(), &*action, cx)
736                                }),
737                        ),
738                )
739            });
740
741        h_flex().gap_0p5().children(buttons)
742    }
743}
744
745impl StatusItemView for PanelButtons {
746    fn set_active_pane_item(
747        &mut self,
748        _active_pane_item: Option<&dyn crate::ItemHandle>,
749        _cx: &mut ViewContext<Self>,
750    ) {
751        // Nothing to do, panel buttons don't depend on the active center item
752    }
753}
754
755#[cfg(any(test, feature = "test-support"))]
756pub mod test {
757    use super::*;
758    use gpui::{actions, div, ViewContext, WindowContext};
759
760    pub struct TestPanel {
761        pub position: DockPosition,
762        pub zoomed: bool,
763        pub active: bool,
764        pub focus_handle: FocusHandle,
765        pub size: Pixels,
766    }
767    actions!(test, [ToggleTestPanel]);
768
769    impl EventEmitter<PanelEvent> for TestPanel {}
770
771    impl TestPanel {
772        pub fn new(position: DockPosition, cx: &mut WindowContext) -> Self {
773            Self {
774                position,
775                zoomed: false,
776                active: false,
777                focus_handle: cx.focus_handle(),
778                size: px(300.),
779            }
780        }
781    }
782
783    impl Render for TestPanel {
784        fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
785            div().id("test").track_focus(&self.focus_handle)
786        }
787    }
788
789    impl Panel for TestPanel {
790        fn persistent_name() -> &'static str {
791            "TestPanel"
792        }
793
794        fn position(&self, _: &gpui::WindowContext) -> super::DockPosition {
795            self.position
796        }
797
798        fn position_is_valid(&self, _: super::DockPosition) -> bool {
799            true
800        }
801
802        fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
803            self.position = position;
804            cx.update_global::<SettingsStore, _>(|_, _| {});
805        }
806
807        fn size(&self, _: &WindowContext) -> Pixels {
808            self.size
809        }
810
811        fn set_size(&mut self, size: Option<Pixels>, _: &mut ViewContext<Self>) {
812            self.size = size.unwrap_or(px(300.));
813        }
814
815        fn icon(&self, _: &WindowContext) -> Option<ui::IconName> {
816            None
817        }
818
819        fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
820            None
821        }
822
823        fn toggle_action(&self) -> Box<dyn Action> {
824            ToggleTestPanel.boxed_clone()
825        }
826
827        fn is_zoomed(&self, _: &WindowContext) -> bool {
828            self.zoomed
829        }
830
831        fn set_zoomed(&mut self, zoomed: bool, _cx: &mut ViewContext<Self>) {
832            self.zoomed = zoomed;
833        }
834
835        fn set_active(&mut self, active: bool, _cx: &mut ViewContext<Self>) {
836            self.active = active;
837        }
838    }
839
840    impl FocusableView for TestPanel {
841        fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
842            self.focus_handle.clone()
843        }
844    }
845}