Skip to main content

sequoia_layout/
dock.rs

1use sequoia_core::{Length, Rect};
2
3use crate::container::Axis;
4
5#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
6pub enum Dock {
7    Left,
8
9    Right,
10
11    Top,
12
13    Bottom,
14
15    #[default]
16    Fill,
17}
18
19impl Dock {
20    pub const fn is_horizontal(self) -> bool {
21        matches!(self, Self::Left | Self::Right)
22    }
23
24    pub const fn is_vertical(self) -> bool {
25        matches!(self, Self::Top | Self::Bottom)
26    }
27
28    pub const fn is_side(self) -> bool {
29        !matches!(self, Self::Fill)
30    }
31}
32
33#[derive(Clone, Debug, PartialEq)]
34pub struct DockStyle {
35    pub docks: Vec<Dock>,
36
37    pub gap: f32,
38
39    pub last_child_fill: bool,
40
41    pub ordered: bool,
42}
43
44impl Default for DockStyle {
45    fn default() -> Self {
46        Self {
47            docks: Vec::new(),
48            gap: 0.0,
49            last_child_fill: false,
50            ordered: false,
51        }
52    }
53}
54
55impl DockStyle {
56    pub fn new(docks: impl IntoIterator<Item = Dock>) -> Self {
57        Self {
58            docks: docks.into_iter().collect(),
59            gap: 0.0,
60            last_child_fill: false,
61            ordered: false,
62        }
63    }
64
65    pub fn gap(mut self, gap: f32) -> Self {
66        self.gap = sanitize_gap(gap);
67        self
68    }
69
70    pub fn last_child_fill(mut self, fill: bool) -> Self {
71        self.last_child_fill = fill;
72        self
73    }
74
75    pub fn ordered(mut self, ordered: bool) -> Self {
76        self.ordered = ordered;
77        self
78    }
79
80    pub fn border(mut self, border: bool) -> Self {
81        self.ordered = !border;
82        self
83    }
84}
85
86pub fn sanitize_gap(gap: f32) -> f32 {
87    if gap.is_finite() { gap.max(0.0) } else { 0.0 }
88}
89
90pub fn dock_thickness(len: Length, intrinsic: f32, available: f32) -> f32 {
91    let intrinsic = if intrinsic.is_finite() {
92        intrinsic.max(0.0)
93    } else {
94        0.0
95    };
96    match len {
97        Length::Px(v) => {
98            if v.is_finite() {
99                v.max(0.0)
100            } else {
101                intrinsic
102            }
103        }
104        Length::Percent(p) => {
105            if available.is_finite() && p.is_finite() {
106                (available * p / 100.0).max(0.0)
107            } else {
108                intrinsic
109            }
110        }
111        Length::Fill(_) | Length::Fit => intrinsic,
112    }
113}
114
115pub fn effective_dock(docks: &[Dock], last_child_fill: bool, index: usize, count: usize) -> Dock {
116    if count > 0 && last_child_fill && index + 1 == count {
117        return Dock::Fill;
118    }
119    docks.get(index).copied().unwrap_or(Dock::Fill)
120}
121
122pub fn resolve_dock(
123    docks: &[Dock],
124    last_child_fill: bool,
125    ordered: bool,
126    index: usize,
127    count: usize,
128) -> Dock {
129    if ordered {
130        effective_dock(docks, last_child_fill, index, count)
131    } else {
132        docks.get(index).copied().unwrap_or(Dock::Fill)
133    }
134}
135
136pub fn dock_layout_order(
137    docks: &[Dock],
138    last_child_fill: bool,
139    ordered: bool,
140    count: usize,
141) -> Vec<usize> {
142    let mut order: Vec<usize> = (0..count).collect();
143    if ordered || count == 0 {
144        return order;
145    }
146    order.sort_by_key(|&i| match docks.get(i).copied().unwrap_or(Dock::Fill) {
147        Dock::Top | Dock::Bottom => 0,
148        Dock::Left | Dock::Right => 1,
149        Dock::Fill => 2,
150    });
151    let _ = last_child_fill;
152    order
153}
154
155pub fn sanitize_frac(frac: f32) -> f32 {
156    if frac.is_finite() {
157        frac.clamp(0.0, 1.0)
158    } else {
159        0.5
160    }
161}
162
163pub fn sanitize_preview_frac(frac: f32) -> f32 {
164    if frac.is_finite() {
165        frac.clamp(0.15, 0.85)
166    } else {
167        0.5
168    }
169}
170
171pub fn split_axis_for_dock(dock: Dock) -> Option<(Axis, bool)> {
172    match dock {
173        Dock::Left => Some((Axis::Horizontal, true)),
174        Dock::Right => Some((Axis::Horizontal, false)),
175        Dock::Top => Some((Axis::Vertical, true)),
176        Dock::Bottom => Some((Axis::Vertical, false)),
177        Dock::Fill => None,
178    }
179}
180
181pub fn dock_preview_rect(host: Rect, dock: Dock, frac: f32) -> Rect {
182    let f = sanitize_preview_frac(frac);
183    let x = host.origin.x;
184    let y = host.origin.y;
185    let w = host.size.width.max(0.0);
186    let h = host.size.height.max(0.0);
187    match dock {
188        Dock::Left => Rect::from_xywh(x, y, w * f, h),
189        Dock::Right => Rect::from_xywh(x + w * (1.0 - f), y, w * f, h),
190        Dock::Top => Rect::from_xywh(x, y, w, h * f),
191        Dock::Bottom => Rect::from_xywh(x, y + h * (1.0 - f), w, h * f),
192        Dock::Fill => host,
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199
200    fn approx(a: f32, b: f32) -> bool {
201        (a - b).abs() < 0.01
202    }
203
204    #[test]
205    fn px_stays_and_percent_scales_with_available() {
206        assert!(approx(
207            dock_thickness(Length::Px(100.0), 40.0, 400.0),
208            100.0
209        ));
210        assert!(approx(
211            dock_thickness(Length::Percent(25.0), 40.0, 400.0),
212            100.0
213        ));
214    }
215
216    #[test]
217    fn auto_and_fill_take_intrinsic() {
218        assert!(approx(dock_thickness(Length::Fit, 70.0, 400.0), 70.0));
219        assert!(approx(dock_thickness(Length::Fill(1.0), 70.0, 400.0), 70.0));
220    }
221
222    #[test]
223    fn unbounded_axis_falls_back_to_content() {
224        assert!(approx(
225            dock_thickness(Length::Percent(50.0), 80.0, f32::INFINITY),
226            80.0
227        ));
228    }
229
230    #[test]
231    fn negative_and_non_finite_inputs_clamp_to_zero() {
232        assert!(approx(dock_thickness(Length::Px(-5.0), 10.0, 400.0), 0.0));
233        assert!(approx(dock_thickness(Length::Fit, -3.0, 400.0), 0.0));
234        assert!(approx(dock_thickness(Length::Fit, f32::NAN, 400.0), 0.0));
235    }
236
237    #[test]
238    fn effective_dock_last_child_fill_wins() {
239        let docks = [Dock::Left, Dock::Left];
240        assert_eq!(effective_dock(&docks, true, 0, 2), Dock::Left);
241        assert_eq!(effective_dock(&docks, true, 1, 2), Dock::Fill);
242        assert_eq!(effective_dock(&docks, false, 1, 2), Dock::Left);
243
244        assert_eq!(effective_dock(&[], true, 0, 1), Dock::Fill);
245        assert_eq!(effective_dock(&[Dock::Top], false, 3, 4), Dock::Fill);
246    }
247
248    #[test]
249    fn gap_sanitizes() {
250        assert!(approx(sanitize_gap(10.0), 10.0));
251        assert!(approx(sanitize_gap(-2.0), 0.0));
252        assert!(approx(sanitize_gap(f32::NAN), 0.0));
253        assert!(approx(sanitize_gap(f32::INFINITY), 0.0));
254    }
255
256    #[test]
257    fn frac_sanitizes_like_split_panel() {
258        assert_eq!(sanitize_frac(2.0), 1.0);
259        assert_eq!(sanitize_frac(-1.0), 0.0);
260        assert_eq!(sanitize_frac(f32::NAN), 0.5);
261        assert_eq!(sanitize_preview_frac(99.0), 0.85);
262        assert_eq!(sanitize_preview_frac(f32::NAN), 0.5);
263    }
264
265    #[test]
266    fn split_axis_maps_sides() {
267        use crate::container::Axis;
268        assert_eq!(
269            split_axis_for_dock(Dock::Left),
270            Some((Axis::Horizontal, true))
271        );
272        assert_eq!(
273            split_axis_for_dock(Dock::Right),
274            Some((Axis::Horizontal, false))
275        );
276        assert_eq!(split_axis_for_dock(Dock::Top), Some((Axis::Vertical, true)));
277        assert_eq!(
278            split_axis_for_dock(Dock::Bottom),
279            Some((Axis::Vertical, false))
280        );
281        assert_eq!(split_axis_for_dock(Dock::Fill), None);
282    }
283
284    #[test]
285    fn preview_rect_splits_host() {
286        use sequoia_core::Rect;
287        let host = Rect::from_xywh(0.0, 0.0, 400.0, 300.0);
288        let l = dock_preview_rect(host, Dock::Left, 0.5);
289        assert!(approx(l.size.width, 200.0));
290        assert!(approx(l.size.height, 300.0));
291        let b = dock_preview_rect(host, Dock::Bottom, 0.25);
292        assert!(approx(b.size.height, 75.0));
293        assert_eq!(dock_preview_rect(host, Dock::Fill, 0.5), host);
294    }
295
296    #[test]
297    fn border_order_groups_sides_before_fill() {
298        use Dock::*;
299        let docks = [Left, Fill, Bottom, Right, Top];
300
301        assert_eq!(
302            dock_layout_order(&docks, false, false, 5),
303            vec![2, 4, 0, 3, 1]
304        );
305
306        assert_eq!(
307            dock_layout_order(&docks, false, true, 5),
308            vec![0, 1, 2, 3, 4]
309        );
310    }
311
312    #[test]
313    fn border_ignores_last_child_fill() {
314        use Dock::*;
315        let docks = [Left, Left];
316        assert_eq!(resolve_dock(&docks, true, false, 1, 2), Left);
317        assert_eq!(resolve_dock(&docks, true, true, 1, 2), Fill);
318        assert!(!DockStyle::default().ordered);
319        assert!(!DockStyle::default().last_child_fill);
320    }
321}