dock.rs

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