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