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