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