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