Skip to main content

gpui_base/dock/
dock_placement.rs

1//! Pure size arithmetic for resizing one dock (left/right/bottom), and the
2//! dock's runtime state (open/collapsible/size/resizing).
3//!
4//! This module decides *how big a dock is allowed to be*. It draws nothing:
5//! the resize-handle chrome and collapsed/expanded presentation live in
6//! `crates/component`.
7
8use gpui::{Bounds, Pixels, Point, px};
9
10use crate::PANEL_MIN_SIZE;
11
12use super::state::DockPlacement;
13
14/// Pure arithmetic for resizing one dock. The caller supplies the area
15/// bounds and the opposite dock's size; base does not reach across entities
16/// to find them — the original `Dock::resize` read sibling dock sizes
17/// straight off the (application-owned) `DockArea` entity, which base has no
18/// way to do.
19#[derive(Clone, Copy, Debug)]
20pub struct DockSizing {
21    placement: DockPlacement,
22    area: Bounds<Pixels>,
23    opposite_dock_size: Pixels,
24}
25
26impl DockSizing {
27    pub fn new(placement: DockPlacement) -> Self {
28        Self {
29            placement,
30            area: Bounds::default(),
31            opposite_dock_size: px(0.),
32        }
33    }
34
35    /// Set the full area bounds (origin and size). Needed whenever the
36    /// dock's placement depends on the area's origin, e.g. a bottom dock
37    /// measuring from the area's bottom edge.
38    pub fn with_area_bounds(mut self, area: Bounds<Pixels>) -> Self {
39        self.area = area;
40        self
41    }
42
43    /// Set only the area's width, leaving its origin and height untouched.
44    /// Convenient for left/right dock clamping, which only reads
45    /// `area.size.width`.
46    pub fn with_area_width(mut self, width: Pixels) -> Self {
47        self.area.size.width = width;
48        self
49    }
50
51    /// Set only the area's height, leaving its origin and width untouched.
52    /// Convenient for bottom dock clamping, which only reads
53    /// `area.size.height`.
54    pub fn with_area_height(mut self, height: Pixels) -> Self {
55        self.area.size.height = height;
56        self
57    }
58
59    /// Set the size of the dock on the opposite side (right, for a left
60    /// dock; left, for a right dock). Bottom docks have no opposite side.
61    pub fn with_opposite_dock_size(mut self, size: Pixels) -> Self {
62        self.opposite_dock_size = size;
63        self
64    }
65
66    /// The dock size the pointer implies, before clamping.
67    ///
68    /// `DockPlacement::Center` names the canvas, which has no edge to measure
69    /// from and no dock size, so it answers zero. Both this and [`Self::clamp`]
70    /// are public and take a `pub` enum, so a caller can name that variant; a
71    /// base-layer arithmetic helper must not panic a desktop application over
72    /// it. [`DockPlacement::axis`] resolves the same variant the same way,
73    /// silently rather than by panicking.
74    pub fn size_from_pointer(&self, pointer: Point<Pixels>) -> Pixels {
75        match self.placement {
76            DockPlacement::Left => pointer.x - self.area.left(),
77            DockPlacement::Right => self.area.right() - pointer.x,
78            DockPlacement::Bottom => self.area.bottom() - pointer.y,
79            DockPlacement::Center => px(0.),
80        }
81    }
82
83    /// Clamp a size into the range this dock may occupy: never below
84    /// `PANEL_MIN_SIZE`, and never so large it would squeeze the opposite
85    /// dock (if any) below `PANEL_MIN_SIZE` either. The `.max(PANEL_MIN_SIZE)`
86    /// on the computed maximum matters when the area itself is narrower than
87    /// both minimums combined — it keeps the clamp range non-empty.
88    ///
89    /// `DockPlacement::Center` constrains nothing, so it hands the size back
90    /// unchanged. See [`Self::size_from_pointer`] for why that variant is
91    /// answered rather than rejected.
92    pub fn clamp(&self, size: Pixels) -> Pixels {
93        let max_size = match self.placement {
94            DockPlacement::Left | DockPlacement::Right => {
95                (self.area.size.width - PANEL_MIN_SIZE - self.opposite_dock_size)
96                    .max(PANEL_MIN_SIZE)
97            }
98            DockPlacement::Bottom => (self.area.size.height - PANEL_MIN_SIZE).max(PANEL_MIN_SIZE),
99            DockPlacement::Center => return size,
100        };
101        size.clamp(PANEL_MIN_SIZE, max_size)
102    }
103}
104
105/// Runtime state for one dock: whether it is open, collapsible, its current
106/// size, and whether it is mid-resize.
107///
108/// This does not include the dock's placement or its panel content. `DockArea`
109/// owns one of these per dock, paired with that dock's `PaneTree` and keyed
110/// by its [`DockPlacement`]; the placement is the key, and the content is the
111/// tree.
112#[derive(Clone, Copy, Debug)]
113pub struct Dock {
114    open: bool,
115    collapsible: bool,
116    size: Pixels,
117    resizing: bool,
118}
119
120impl Dock {
121    pub fn new(size: Pixels) -> Self {
122        Self {
123            open: true,
124            collapsible: true,
125            size,
126            resizing: false,
127        }
128    }
129
130    pub fn is_open(&self) -> bool {
131        self.open
132    }
133
134    pub fn set_open(&mut self, open: bool) {
135        self.open = open;
136    }
137
138    pub fn is_collapsible(&self) -> bool {
139        self.collapsible
140    }
141
142    pub fn set_collapsible(&mut self, collapsible: bool) {
143        self.collapsible = collapsible;
144    }
145
146    pub fn size(&self) -> Pixels {
147        self.size
148    }
149
150    /// Set the dock's size, never below [`PANEL_MIN_SIZE`].
151    ///
152    /// The floor is here rather than at the call sites because
153    /// `DockArea::set_dock_size` is public and unclamped: a smaller value
154    /// would collapse the dock to nothing, the skin clips the resize handle
155    /// that would drag it back out, and the collapsed size persists.
156    pub fn set_size(&mut self, size: Pixels) {
157        self.size = size.max(PANEL_MIN_SIZE);
158    }
159
160    pub fn is_resizing(&self) -> bool {
161        self.resizing
162    }
163
164    pub fn set_resizing(&mut self, resizing: bool) {
165        self.resizing = resizing;
166    }
167}
168
169#[cfg(test)]
170mod dock_tests {
171    use super::*;
172    use gpui::{point, size};
173
174    #[test]
175    fn a_left_dock_cannot_squeeze_past_the_right_dock() {
176        let sizing = DockSizing::new(DockPlacement::Left)
177            .with_area_width(px(1000.))
178            .with_opposite_dock_size(px(300.));
179
180        assert_eq!(
181            sizing.clamp(px(900.)),
182            px(1000.) - PANEL_MIN_SIZE - px(300.)
183        );
184    }
185
186    #[test]
187    fn a_dock_never_clamps_below_the_minimum() {
188        let sizing = DockSizing::new(DockPlacement::Bottom).with_area_height(px(120.));
189        assert_eq!(sizing.clamp(px(1.)), PANEL_MIN_SIZE);
190    }
191
192    #[test]
193    fn a_bottom_dock_measures_from_the_area_bottom() {
194        let sizing = DockSizing::new(DockPlacement::Bottom).with_area_bounds(Bounds {
195            origin: point(px(0.), px(0.)),
196            size: size(px(800.), px(600.)),
197        });
198
199        assert_eq!(
200            sizing.size_from_pointer(point(px(400.), px(400.))),
201            px(200.)
202        );
203    }
204
205    #[test]
206    fn a_right_dock_measures_from_the_area_right_edge() {
207        let sizing = DockSizing::new(DockPlacement::Right).with_area_bounds(Bounds {
208            origin: point(px(0.), px(0.)),
209            size: size(px(800.), px(600.)),
210        });
211
212        assert_eq!(sizing.size_from_pointer(point(px(500.), px(0.))), px(300.));
213    }
214
215    /// `DockArea::set_dock_size` is public and hands its argument straight
216    /// here, so without this floor a caller could collapse a dock to nothing
217    /// and persist it that way.
218    #[test]
219    fn a_dock_never_shrinks_below_the_minimum() {
220        let mut dock = Dock::new(px(240.));
221        dock.set_size(px(1.));
222        assert_eq!(dock.size(), PANEL_MIN_SIZE);
223
224        dock.set_size(px(400.));
225        assert_eq!(dock.size(), px(400.), "a size above the floor is kept");
226    }
227
228    /// Both are `pub` and take a `pub` enum, so a caller can name the center.
229    /// Answering it is what keeps a base arithmetic helper from panicking a
230    /// desktop application over a value its own type permits.
231    #[test]
232    fn the_center_placement_is_answered_rather_than_panicking() {
233        let sizing = DockSizing::new(DockPlacement::Center).with_area_bounds(Bounds {
234            origin: point(px(0.), px(0.)),
235            size: size(px(800.), px(600.)),
236        });
237
238        assert_eq!(sizing.clamp(px(7.)), px(7.), "the center clamps nothing");
239        assert_eq!(sizing.size_from_pointer(point(px(400.), px(300.))), px(0.));
240    }
241
242    #[test]
243    fn dock_state_defaults_to_open_and_collapsible() {
244        let dock = Dock::new(px(240.));
245        assert!(dock.is_open());
246        assert!(dock.is_collapsible());
247        assert_eq!(dock.size(), px(240.));
248        assert!(!dock.is_resizing());
249    }
250}