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