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