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}