gpui_component/dock/
dock.rs

1//! Dock is a fixed container that places at left, bottom, right of the Windows.
2
3use std::{ops::Deref, sync::Arc};
4
5use gpui::{
6    App, AppContext, Axis, Context, Element, Empty, Entity, IntoElement, MouseMoveEvent,
7    MouseUpEvent, ParentElement as _, Pixels, Point, Render, Style, StyleRefinement, Styled as _,
8    WeakEntity, Window, div, prelude::FluentBuilder as _, px,
9};
10use serde::{Deserialize, Serialize};
11
12use crate::{
13    StyledExt,
14    resizable::{PANEL_MIN_SIZE, resize_handle},
15};
16
17use super::{DockArea, DockItem, PanelView, TabPanel};
18
19#[derive(Clone)]
20struct ResizePanel;
21
22impl Render for ResizePanel {
23    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
24        Empty
25    }
26}
27
28#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
29pub enum DockPlacement {
30    #[serde(rename = "center")]
31    Center,
32    #[serde(rename = "left")]
33    Left,
34    #[serde(rename = "bottom")]
35    Bottom,
36    #[serde(rename = "right")]
37    Right,
38}
39
40impl DockPlacement {
41    fn axis(&self) -> Axis {
42        match self {
43            Self::Left | Self::Right => Axis::Horizontal,
44            Self::Bottom => Axis::Vertical,
45            Self::Center => unreachable!(),
46        }
47    }
48
49    pub fn is_left(&self) -> bool {
50        matches!(self, Self::Left)
51    }
52
53    pub fn is_bottom(&self) -> bool {
54        matches!(self, Self::Bottom)
55    }
56
57    pub fn is_right(&self) -> bool {
58        matches!(self, Self::Right)
59    }
60}
61
62/// The Dock is a fixed container that places at left, bottom, right of the Windows.
63///
64/// This is unlike Panel, it can't be move or add any other panel.
65pub struct Dock {
66    pub(super) placement: DockPlacement,
67    dock_area: WeakEntity<DockArea>,
68    pub(crate) panel: DockItem,
69    /// The size is means the width or height of the Dock, if the placement is left or right, the size is width, otherwise the size is height.
70    pub(super) size: Pixels,
71    pub(super) open: bool,
72    /// Whether the Dock is collapsible, default: true
73    pub(super) collapsible: bool,
74
75    // Runtime state
76    /// Whether the Dock is resizing
77    resizing: bool,
78}
79
80impl Dock {
81    pub(crate) fn new(
82        dock_area: WeakEntity<DockArea>,
83        placement: DockPlacement,
84        window: &mut Window,
85        cx: &mut Context<Self>,
86    ) -> Self {
87        let panel = cx.new(|cx| {
88            let mut tab = TabPanel::new(None, dock_area.clone(), window, cx);
89            tab.closable = false;
90            tab
91        });
92
93        let panel = DockItem::Tabs {
94            size: None,
95            items: Vec::new(),
96            active_ix: 0,
97            view: panel.clone(),
98        };
99
100        Self::subscribe_panel_events(dock_area.clone(), &panel, window, cx);
101
102        Self {
103            placement,
104            dock_area,
105            panel,
106            open: true,
107            collapsible: true,
108            size: px(200.0),
109            resizing: false,
110        }
111    }
112
113    pub fn left(
114        dock_area: WeakEntity<DockArea>,
115        window: &mut Window,
116        cx: &mut Context<Self>,
117    ) -> Self {
118        Self::new(dock_area, DockPlacement::Left, window, cx)
119    }
120
121    pub fn bottom(
122        dock_area: WeakEntity<DockArea>,
123        window: &mut Window,
124        cx: &mut Context<Self>,
125    ) -> Self {
126        Self::new(dock_area, DockPlacement::Bottom, window, cx)
127    }
128
129    pub fn right(
130        dock_area: WeakEntity<DockArea>,
131        window: &mut Window,
132        cx: &mut Context<Self>,
133    ) -> Self {
134        Self::new(dock_area, DockPlacement::Right, window, cx)
135    }
136
137    /// Update the Dock to be collapsible or not.
138    ///
139    /// And if the Dock is not collapsible, it will be open.
140    pub fn set_collapsible(&mut self, collapsible: bool, _: &mut Window, cx: &mut Context<Self>) {
141        self.collapsible = collapsible;
142        if !collapsible {
143            self.open = true
144        }
145        cx.notify();
146    }
147
148    pub(super) fn from_state(
149        dock_area: WeakEntity<DockArea>,
150        placement: DockPlacement,
151        size: Pixels,
152        panel: DockItem,
153        open: bool,
154        window: &mut Window,
155        cx: &mut Context<Self>,
156    ) -> Self {
157        Self::subscribe_panel_events(dock_area.clone(), &panel, window, cx);
158
159        if !open {
160            match panel.clone() {
161                DockItem::Tabs { view, .. } => {
162                    view.update(cx, |panel, cx| {
163                        panel.set_collapsed(true, window, cx);
164                    });
165                }
166                DockItem::Split { items, .. } => {
167                    for item in items {
168                        item.set_collapsed(true, window, cx);
169                    }
170                }
171                _ => {}
172            }
173        }
174
175        Self {
176            placement,
177            dock_area,
178            panel,
179            open,
180            size,
181            collapsible: true,
182            resizing: false,
183        }
184    }
185
186    fn subscribe_panel_events(
187        dock_area: WeakEntity<DockArea>,
188        panel: &DockItem,
189        window: &mut Window,
190        cx: &mut Context<Self>,
191    ) {
192        match panel {
193            DockItem::Tabs { view, .. } => {
194                window.defer(cx, {
195                    let view = view.clone();
196                    move |window, cx| {
197                        _ = dock_area.update(cx, |this, cx| {
198                            this.subscribe_panel(&view, window, cx);
199                        });
200                    }
201                });
202            }
203            DockItem::Split { items, view, .. } => {
204                for item in items {
205                    Self::subscribe_panel_events(dock_area.clone(), item, window, cx);
206                }
207                window.defer(cx, {
208                    let view = view.clone();
209                    move |window, cx| {
210                        _ = dock_area.update(cx, |this, cx| {
211                            this.subscribe_panel(&view, window, cx);
212                        });
213                    }
214                });
215            }
216            DockItem::Tiles { view, .. } => {
217                window.defer(cx, {
218                    let view = view.clone();
219                    move |window, cx| {
220                        _ = dock_area.update(cx, |this, cx| {
221                            this.subscribe_panel(&view, window, cx);
222                        });
223                    }
224                });
225            }
226            DockItem::Panel { .. } => {
227                // Not supported
228            }
229        }
230    }
231
232    pub fn set_panel(&mut self, panel: DockItem, _: &mut Window, cx: &mut Context<Self>) {
233        self.panel = panel;
234        cx.notify();
235    }
236
237    pub fn is_open(&self) -> bool {
238        self.open
239    }
240
241    pub fn toggle_open(&mut self, window: &mut Window, cx: &mut Context<Self>) {
242        self.set_open(!self.open, window, cx);
243    }
244
245    /// Returns the size of the Dock, the size is means the width or height of
246    /// the Dock, if the placement is left or right, the size is width,
247    /// otherwise the size is height.
248    pub fn size(&self) -> Pixels {
249        self.size
250    }
251
252    /// Set the size of the Dock.
253    pub fn set_size(&mut self, size: Pixels, _: &mut Window, cx: &mut Context<Self>) {
254        self.size = size.max(PANEL_MIN_SIZE);
255        cx.notify();
256    }
257
258    /// Set the open state of the Dock.
259    pub fn set_open(&mut self, open: bool, window: &mut Window, cx: &mut Context<Self>) {
260        self.open = open;
261        let item = self.panel.clone();
262        cx.defer_in(window, move |_, window, cx| {
263            item.set_collapsed(!open, window, cx);
264        });
265        cx.notify();
266    }
267
268    /// Add item to the Dock.
269    pub fn add_panel(
270        &mut self,
271        panel: Arc<dyn PanelView>,
272        window: &mut Window,
273        cx: &mut Context<Self>,
274    ) {
275        self.panel
276            .add_panel(panel, &self.dock_area, None, window, cx);
277        cx.notify();
278    }
279
280    /// Remove item from the Dock.
281    pub fn remove_panel(
282        &mut self,
283        panel: Arc<dyn PanelView>,
284        window: &mut Window,
285        cx: &mut Context<Self>,
286    ) {
287        self.panel.remove_panel(panel, window, cx);
288        cx.notify();
289    }
290
291    fn render_resize_handle(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
292        let axis = self.placement.axis();
293        let view = cx.entity().clone();
294
295        resize_handle("resize-handle", axis)
296            .placement(self.placement)
297            .on_drag(ResizePanel {}, move |info, _, _, cx| {
298                cx.stop_propagation();
299                view.update(cx, |view, _| {
300                    view.resizing = true;
301                });
302                cx.new(|_| info.deref().clone())
303            })
304    }
305    fn resize(&mut self, mouse_position: Point<Pixels>, _: &mut Window, cx: &mut Context<Self>) {
306        if !self.resizing {
307            return;
308        }
309
310        let dock_area = self
311            .dock_area
312            .upgrade()
313            .expect("DockArea is missing")
314            .read(cx);
315        let area_bounds = dock_area.bounds;
316        let mut left_dock_size = px(0.0);
317        let mut right_dock_size = px(0.0);
318
319        // Get the size of the left dock if it's open and not the current dock
320        if let Some(left_dock) = &dock_area.left_dock {
321            if left_dock.entity_id() != cx.entity().entity_id() {
322                let left_dock_read = left_dock.read(cx);
323                if left_dock_read.is_open() {
324                    left_dock_size = left_dock_read.size;
325                }
326            }
327        }
328
329        // Get the size of the right dock if it's open and not the current dock
330        if let Some(right_dock) = &dock_area.right_dock {
331            if right_dock.entity_id() != cx.entity().entity_id() {
332                let right_dock_read = right_dock.read(cx);
333                if right_dock_read.is_open() {
334                    right_dock_size = right_dock_read.size;
335                }
336            }
337        }
338
339        let size = match self.placement {
340            DockPlacement::Left => mouse_position.x - area_bounds.left(),
341            DockPlacement::Right => area_bounds.right() - mouse_position.x,
342            DockPlacement::Bottom => area_bounds.bottom() - mouse_position.y,
343            DockPlacement::Center => unreachable!(),
344        };
345        match self.placement {
346            DockPlacement::Left => {
347                let max_size = area_bounds.size.width - PANEL_MIN_SIZE - right_dock_size;
348                self.size = size.clamp(PANEL_MIN_SIZE, max_size);
349            }
350            DockPlacement::Right => {
351                let max_size = area_bounds.size.width - PANEL_MIN_SIZE - left_dock_size;
352                self.size = size.clamp(PANEL_MIN_SIZE, max_size);
353            }
354            DockPlacement::Bottom => {
355                let max_size = area_bounds.size.height - PANEL_MIN_SIZE;
356                self.size = size.clamp(PANEL_MIN_SIZE, max_size);
357            }
358            DockPlacement::Center => unreachable!(),
359        }
360
361        cx.notify();
362    }
363
364    fn done_resizing(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {
365        self.resizing = false;
366    }
367}
368
369impl Render for Dock {
370    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl gpui::IntoElement {
371        if !self.open && !self.placement.is_bottom() {
372            return div();
373        }
374
375        let cache_style = StyleRefinement::default().absolute().size_full();
376
377        div()
378            .relative()
379            .overflow_hidden()
380            .map(|this| match self.placement {
381                DockPlacement::Left | DockPlacement::Right => this.h_flex().h_full().w(self.size),
382                DockPlacement::Bottom => this.w_full().h(self.size),
383                DockPlacement::Center => unreachable!(),
384            })
385            // Bottom Dock should keep the title bar, then user can click the Toggle button
386            .when(!self.open && self.placement.is_bottom(), |this| {
387                this.h(px(29.))
388            })
389            .map(|this| match &self.panel {
390                DockItem::Split { view, .. } => this.child(view.clone()),
391                DockItem::Tabs { view, .. } => this.child(view.clone()),
392                DockItem::Panel { view, .. } => this.child(view.clone().view().cached(cache_style)),
393                // Not support to render Tiles and Tile into Dock
394                DockItem::Tiles { .. } => this,
395            })
396            .child(self.render_resize_handle(window, cx))
397            .child(DockElement {
398                view: cx.entity().clone(),
399            })
400    }
401}
402
403struct DockElement {
404    view: Entity<Dock>,
405}
406
407impl IntoElement for DockElement {
408    type Element = Self;
409
410    fn into_element(self) -> Self::Element {
411        self
412    }
413}
414
415impl Element for DockElement {
416    type RequestLayoutState = ();
417    type PrepaintState = ();
418
419    fn id(&self) -> Option<gpui::ElementId> {
420        None
421    }
422
423    fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
424        None
425    }
426
427    fn request_layout(
428        &mut self,
429        _: Option<&gpui::GlobalElementId>,
430        _: Option<&gpui::InspectorElementId>,
431        window: &mut gpui::Window,
432        cx: &mut App,
433    ) -> (gpui::LayoutId, Self::RequestLayoutState) {
434        (window.request_layout(Style::default(), None, cx), ())
435    }
436
437    fn prepaint(
438        &mut self,
439        _: Option<&gpui::GlobalElementId>,
440        _: Option<&gpui::InspectorElementId>,
441        _: gpui::Bounds<Pixels>,
442        _: &mut Self::RequestLayoutState,
443        _window: &mut gpui::Window,
444        _cx: &mut App,
445    ) -> Self::PrepaintState {
446        ()
447    }
448
449    fn paint(
450        &mut self,
451        _: Option<&gpui::GlobalElementId>,
452        _: Option<&gpui::InspectorElementId>,
453        _: gpui::Bounds<Pixels>,
454        _: &mut Self::RequestLayoutState,
455        _: &mut Self::PrepaintState,
456        window: &mut gpui::Window,
457        cx: &mut App,
458    ) {
459        window.on_mouse_event({
460            let view = self.view.clone();
461            let resizing = view.read(cx).resizing;
462            move |e: &MouseMoveEvent, phase, window, cx| {
463                if !resizing {
464                    return;
465                }
466                if !phase.bubble() {
467                    return;
468                }
469
470                view.update(cx, |view, cx| view.resize(e.position, window, cx))
471            }
472        });
473
474        // When any mouse up, stop dragging
475        window.on_mouse_event({
476            let view = self.view.clone();
477            move |_: &MouseUpEvent, phase, window, cx| {
478                if phase.bubble() {
479                    view.update(cx, |view, cx| view.done_resizing(window, cx));
480                }
481            }
482        })
483    }
484}