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    live_size: Option<Pixels>,
118    resizing: bool,
119}
120
121impl Dock {
122    pub fn new(size: Pixels) -> Self {
123        Self {
124            open: true,
125            collapsible: true,
126            size,
127            live_size: None,
128            resizing: false,
129        }
130    }
131
132    pub fn is_open(&self) -> bool {
133        self.open
134    }
135
136    pub fn set_open(&mut self, open: bool) {
137        self.open = open;
138    }
139
140    pub fn is_collapsible(&self) -> bool {
141        self.collapsible
142    }
143
144    pub fn set_collapsible(&mut self, collapsible: bool) {
145        self.collapsible = collapsible;
146    }
147
148    pub fn size(&self) -> Pixels {
149        self.size
150    }
151
152    /// Set the dock's size, never below [`PANEL_MIN_SIZE`].
153    ///
154    /// The floor is here rather than at the call sites because
155    /// `DockArea::set_dock_size` is public and unclamped: a smaller value
156    /// would collapse the dock to nothing, the skin clips the resize handle
157    /// that would drag it back out, and the collapsed size persists.
158    pub fn set_size(&mut self, size: Pixels) {
159        self.size = size.max(PANEL_MIN_SIZE);
160    }
161
162    /// A size below [`PANEL_MIN_SIZE`] that a drag in progress is showing.
163    /// It is never persisted: the drag's release settles it into either a
164    /// closed dock or one at the minimum.
165    pub(crate) fn live_size(&self) -> Option<Pixels> {
166        self.live_size
167    }
168
169    pub(crate) fn set_live_size(&mut self, size: Option<Pixels>) {
170        self.live_size = size;
171    }
172
173    pub fn is_resizing(&self) -> bool {
174        self.resizing
175    }
176
177    pub fn set_resizing(&mut self, resizing: bool) {
178        self.resizing = resizing;
179    }
180}
181
182#[cfg(test)]
183mod dock_tests {
184    use super::*;
185    use gpui::{point, size};
186
187    #[test]
188    fn a_left_dock_cannot_squeeze_past_the_right_dock() {
189        let sizing = DockSizing::new(DockPlacement::Left)
190            .with_area_width(px(1000.))
191            .with_opposite_dock_size(px(300.));
192
193        assert_eq!(
194            sizing.clamp(px(900.)),
195            px(1000.) - PANEL_MIN_SIZE - px(300.)
196        );
197    }
198
199    #[test]
200    fn a_dock_never_clamps_below_the_minimum() {
201        let sizing = DockSizing::new(DockPlacement::Bottom).with_area_height(px(120.));
202        assert_eq!(sizing.clamp(px(1.)), PANEL_MIN_SIZE);
203    }
204
205    #[test]
206    fn a_bottom_dock_measures_from_the_area_bottom() {
207        let sizing = DockSizing::new(DockPlacement::Bottom).with_area_bounds(Bounds {
208            origin: point(px(0.), px(0.)),
209            size: size(px(800.), px(600.)),
210        });
211
212        assert_eq!(
213            sizing.size_from_pointer(point(px(400.), px(400.))),
214            px(200.)
215        );
216    }
217
218    #[test]
219    fn a_right_dock_measures_from_the_area_right_edge() {
220        let sizing = DockSizing::new(DockPlacement::Right).with_area_bounds(Bounds {
221            origin: point(px(0.), px(0.)),
222            size: size(px(800.), px(600.)),
223        });
224
225        assert_eq!(sizing.size_from_pointer(point(px(500.), px(0.))), px(300.));
226    }
227
228    /// `DockArea::set_dock_size` is public and hands its argument straight
229    /// here, so without this floor a caller could collapse a dock to nothing
230    /// and persist it that way.
231    #[test]
232    fn a_dock_never_shrinks_below_the_minimum() {
233        let mut dock = Dock::new(px(240.));
234        dock.set_size(px(1.));
235        assert_eq!(dock.size(), PANEL_MIN_SIZE);
236
237        dock.set_size(px(400.));
238        assert_eq!(dock.size(), px(400.), "a size above the floor is kept");
239    }
240
241    /// Both are `pub` and take a `pub` enum, so a caller can name the center.
242    /// Answering it is what keeps a base arithmetic helper from panicking a
243    /// desktop application over a value its own type permits.
244    #[test]
245    fn the_center_placement_is_answered_rather_than_panicking() {
246        let sizing = DockSizing::new(DockPlacement::Center).with_area_bounds(Bounds {
247            origin: point(px(0.), px(0.)),
248            size: size(px(800.), px(600.)),
249        });
250
251        assert_eq!(sizing.clamp(px(7.)), px(7.), "the center clamps nothing");
252        assert_eq!(sizing.size_from_pointer(point(px(400.), px(300.))), px(0.));
253    }
254
255    #[test]
256    fn dock_state_defaults_to_open_and_collapsible() {
257        let dock = Dock::new(px(240.));
258        assert!(dock.is_open());
259        assert!(dock.is_collapsible());
260        assert_eq!(dock.size(), px(240.));
261        assert!(!dock.is_resizing());
262    }
263}