feather_ui/layout/
flex.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2025 Fundament Research Institute <https://fundament.institute>
3
4use super::{
5    Concrete, Desc, Layout, Renderable, Staged, base, check_unsized_abs, map_unsized_area,
6    merge_margin, nuetralize_unsized,
7};
8use crate::layout::Swappable;
9use crate::persist::{FnPersist2, Persist2, VectorFold};
10use crate::{
11    DAbsRect, DValue, PxDim, PxLimits, PxPerimeter, PxPoint, PxRect, RelDim, RowDirection,
12    UNSIZED_AXIS, rtree,
13};
14use derive_more::TryFrom;
15use smallvec::SmallVec;
16use std::rc::Rc;
17
18#[derive(Debug, Copy, Clone, PartialEq, Eq, Default, TryFrom, derive_more::Display)]
19#[try_from(repr)]
20#[repr(u8)]
21pub enum FlexJustify {
22    #[default]
23    Start,
24    Center,
25    End,
26    SpaceBetween,
27    SpaceAround,
28    SpaceFull,
29}
30
31pub trait Prop:
32    base::ZIndex + base::Obstacles + base::Limits + base::Direction + base::Area
33{
34    fn wrap(&self) -> bool;
35    fn justify(&self) -> FlexJustify;
36    fn align(&self) -> FlexJustify;
37}
38
39crate::gen_from_to_dyn!(Prop);
40
41pub trait Child: base::Margin + base::RLimits + base::Order {
42    fn grow(&self) -> f32;
43    fn shrink(&self) -> f32;
44    fn basis(&self) -> DValue;
45}
46
47crate::gen_from_to_dyn!(Child);
48
49#[allow(clippy::too_many_arguments)]
50fn next_obstacle(
51    obstacles: &[DAbsRect],
52    max_aux: f32,
53    main: f32,
54    aux: f32,
55    xaxis: bool,
56    total_main: f32,
57    min: &mut usize,
58    dpi: RelDim,
59) -> (f32, f32) {
60    // Given our current X/Y position, what is the next obstacle we would run into?
61    let mut i = *min;
62    while i < obstacles.len() {
63        let obstacle = obstacles[i].resolve(dpi);
64        let (mut start, aux_start) = obstacle.topleft().swap_axis(xaxis);
65
66        if total_main > 0.0 {
67            start = total_main - start;
68        }
69
70        // If we've reached an obstacle whose top/left is past the (current known) bottom of this line, we won't match anything.
71        if aux_start > aux + max_aux {
72            break;
73        }
74
75        let (mut end, aux_end) = obstacle.bottomright().swap_axis(xaxis);
76
77        if total_main > 0.0 {
78            end = total_main - end;
79        }
80
81        // If aux is past past the bottom/right, we can skip this obstacle for all future lines in this wrap attempt.
82        if aux_end < aux {
83            i += 1;
84            *min = i;
85            continue;
86        }
87
88        // If our main axis has gone past this obstacles starting edge, we already passed it, so skip forward
89        if main > start {
90            i += 1;
91            continue;
92        }
93
94        return (start, end);
95    }
96
97    (f32::INFINITY, 0.0)
98}
99
100fn justify_inner_outer(align: FlexJustify, total: f32, used: f32, count: i32) -> (f32, f32) {
101    match align {
102        FlexJustify::SpaceBetween => (0.0, (total - used) / (count - 1) as f32),
103        FlexJustify::SpaceAround => {
104            let r = (total - used) / count as f32;
105            (r / 2.0, r)
106        }
107        FlexJustify::SpaceFull => {
108            let r = (total - used) / (count + 1) as f32;
109            (r, r)
110        }
111        FlexJustify::Center => ((total - used) / 2.0, 0.0),
112        FlexJustify::End => (total - used, 0.0),
113        _ => (0.0, 0.0),
114    }
115}
116
117// Stores information about a calculated linebreak
118struct Linebreak {
119    index: usize,
120    lineheight: f32,
121    skip: f32, // If this is just skipping over an obstacle, tells us where to skip to.
122    aux_margin: f32, // Maximum margin value between lines
123}
124
125impl Linebreak {
126    pub fn new(index: usize, lineheight: f32, skip: f32, aux_margin: f32) -> Self {
127        Self {
128            index,
129            lineheight,
130            skip,
131            aux_margin,
132        }
133    }
134}
135
136#[derive(Clone)]
137struct ChildCache {
138    basis: f32,
139    grow: f32,
140    shrink: f32,
141    aux: f32,
142    margin: PxPerimeter,
143    limits: PxLimits,
144}
145
146#[allow(clippy::too_many_arguments)]
147fn wrap_line(
148    childareas: &im::Vector<Option<ChildCache>>,
149    props: &dyn Prop,
150    xaxis: bool,
151    total_main: f32,
152    total_aux: f32,
153    mut used_aux: f32,
154    mut linecount: i32,
155    justify: FlexJustify,
156    backwards: bool,
157    dpi: RelDim,
158) -> (SmallVec<[Linebreak; 10]>, i32, f32) {
159    let mut breaks: SmallVec<[Linebreak; 10]> = SmallVec::new();
160
161    let (mut aux, inner) = justify_inner_outer(justify, total_aux, used_aux, linecount);
162
163    // Reset linecount and used_aux
164    linecount = 1;
165    used_aux = 0.0;
166    let mut max_aux = 0.0;
167    let mut max_aux_margin: f32 = 0.0;
168    let mut max_aux_upper_margin: f32 = 0.0;
169    let mut main = 0.0;
170    let mut prev_margin = f32::NAN;
171    let mut min_obstacle = 0;
172    let reversed = if backwards { total_main } else { -1.0 };
173    let mut obstacle = next_obstacle(
174        props.obstacles(),
175        max_aux,
176        main,
177        aux,
178        xaxis,
179        reversed,
180        &mut min_obstacle,
181        dpi,
182    );
183
184    let mut i = 0;
185    while i < childareas.len() {
186        let Some(ref b) = childareas[i] else {
187            i += 1;
188            continue;
189        };
190
191        // This is a bit unintuitive, but this is adding the margin for the previous element, not this one.
192        if !prev_margin.is_nan() {
193            main += prev_margin.max(b.margin.topleft().width);
194        }
195
196        // If we hit an obstacle, mark it as an obstacle breakpoint, then jump forward. We ignore margins here, because
197        // obstacles are not items, they are edges.
198        if main + b.basis > obstacle.0 {
199            breaks.push(Linebreak::new(i, obstacle.1, obstacle.0, f32::NAN));
200            main = obstacle.1; // Set the axis to the end of the obstacle
201            prev_margin = f32::NAN; // Reset the margin, because this counts as an edge instead of an item
202
203            obstacle = next_obstacle(
204                props.obstacles(),
205                max_aux,
206                main,
207                aux,
208                xaxis,
209                reversed,
210                &mut min_obstacle,
211                dpi,
212            );
213
214            // We DO NOT update any other values here, nor do we increment i, because we might hit another obstacle
215            // or the end of the line, so we immediately loop around to try again.
216            continue;
217        }
218
219        // Once we hit the end of the line we mark the true breakpoint.
220        if main + b.basis > total_main {
221            // If our line was empty, then nothing could fit on it. Because we don't have line-height information, we simply
222            // have to use the height of the element we are pushing to the next line.
223            let emptyline = if max_aux == 0.0 {
224                max_aux = b.aux;
225
226                // Normally, if an obstacle is present on a line, we want to skip it entirely. However, if we can't fit an item on
227                // a line that has no obstacle, we have to give up and put it there anyway to prevent an infinite loop.
228                if let Some(b) = breaks.last() {
229                    b.lineheight >= 0.0
230                } else {
231                    true
232                }
233            } else {
234                false
235            };
236
237            main = 0.0;
238            if let Some(b) = breaks.last_mut() {
239                b.aux_margin = b.aux_margin.max(max_aux_upper_margin);
240            }
241            breaks.push(Linebreak::new(i, max_aux, f32::INFINITY, max_aux_margin));
242            prev_margin = f32::NAN;
243            used_aux += max_aux;
244            aux += max_aux + inner;
245            max_aux = 0.0;
246            max_aux_margin = 0.0;
247            max_aux_upper_margin = 0.0;
248            linecount += 1;
249            obstacle = next_obstacle(
250                props.obstacles(),
251                max_aux,
252                main,
253                aux,
254                xaxis,
255                reversed,
256                &mut min_obstacle,
257                dpi,
258            );
259
260            if !emptyline {
261                continue;
262            }
263        }
264
265        main += b.basis;
266        prev_margin = b.margin.bottomright().width;
267        max_aux_margin = max_aux_margin.max(b.margin.bottomright().height);
268        max_aux_upper_margin = max_aux_upper_margin.max(b.margin.topleft().height);
269        max_aux = max_aux.max(b.aux);
270        i += 1;
271    }
272
273    if let Some(b) = breaks.last_mut() {
274        b.aux_margin = b.aux_margin.max(max_aux_upper_margin);
275    }
276
277    breaks.push(Linebreak::new(
278        childareas.len(),
279        max_aux,
280        f32::INFINITY,
281        f32::NAN,
282    ));
283
284    (breaks, linecount, used_aux)
285}
286
287impl Desc for dyn Prop {
288    type Props = dyn Prop;
289    type Child = dyn Child;
290    type Children = im::Vector<Option<Box<dyn Layout<Self::Child>>>>;
291
292    fn stage<'a>(
293        props: &Self::Props,
294        outer_area: crate::PxRect,
295        outer_limits: crate::PxLimits,
296        children: &Self::Children,
297        id: std::sync::Weak<crate::SourceID>,
298        renderable: Option<Rc<dyn Renderable>>,
299        window: &mut crate::component::window::WindowState,
300    ) -> Box<dyn Staged + 'a> {
301        use super::Swappable;
302
303        let myarea = props.area().resolve(window.dpi);
304        //let (unsized_x, unsized_y) = super::check_unsized(*myarea);
305
306        let limits = outer_limits + props.limits().resolve(window.dpi);
307        let inner_dim = super::limit_dim(super::eval_dim(myarea, outer_area.dim()), limits);
308        let outer_safe = nuetralize_unsized(outer_area);
309
310        let xaxis = match props.direction() {
311            RowDirection::LeftToRight | RowDirection::RightToLeft => true,
312            RowDirection::TopToBottom | RowDirection::BottomToTop => false,
313        };
314
315        let mut childareas: im::Vector<Option<ChildCache>> = im::Vector::new();
316        let (dpi_main, _) = window.dpi.swap_axis(xaxis);
317        let (outer_main, _) = outer_safe.dim().swap_axis(xaxis);
318
319        // We re-use a lot of concepts from flexbox in this calculation. First we acquire the natural size of all child elements.
320        for child in children.iter() {
321            let imposed = child.as_ref().unwrap().get_props();
322
323            let child_limit = super::apply_limit(inner_dim, limits, *imposed.rlimits());
324            let basis = imposed.basis().resolve(dpi_main).resolve(outer_main);
325
326            assert!(
327                basis.is_finite(),
328                "Basis can be unsized, but never infinite!"
329            );
330
331            let inner_area = PxRect::corners(
332                PxPoint::zero(),
333                if xaxis {
334                    PxPoint::new(basis, UNSIZED_AXIS)
335                } else {
336                    PxPoint::new(UNSIZED_AXIS, basis)
337                },
338            );
339
340            let stage = child
341                .as_ref()
342                .unwrap()
343                .stage(inner_area, child_limit, window);
344
345            let (main, aux) = stage.get_area().dim().swap_axis(xaxis);
346
347            let mut cache = ChildCache {
348                basis,
349                grow: imposed.grow(),
350                shrink: imposed.shrink(),
351                aux,
352                margin: imposed
353                    .margin()
354                    .resolve(window.dpi)
355                    .to_perimeter(outer_safe),
356                limits: child_limit,
357            };
358            if cache.basis == UNSIZED_AXIS {
359                cache.basis = main;
360            }
361
362            // Swap the margin axis if necessary
363            if !xaxis {
364                let ltrb = cache.margin.v.as_array_mut();
365                ltrb.swap(0, 1);
366                ltrb.swap(2, 3);
367            }
368
369            childareas.push_back(Some(cache));
370        }
371
372        // This fold calculates the maximum size of the main axis, followed by the off-axis, followed
373        // by carrying the previous margin amount from the main axis so it can be collapsed properly.
374        // Note that margins only ever apply between elements, not edges, so we completely ignore the
375        // off-axis margin, as this calculation assumes there is only 1 line of items, and the off-axis
376        // margin doesn't apply until there are linebreaks.
377        let mut fold = VectorFold::new(Persist2::new(&|prev: (f32, f32, f32),
378                                                       n: &Option<ChildCache>|
379         -> (f32, f32, f32) {
380            let cache = n.as_ref().unwrap();
381            (
382                cache.basis + prev.0 + merge_margin(prev.2, cache.margin.topleft().width),
383                cache.aux.max(prev.1),
384                cache.margin.bottomright().width,
385            )
386        }));
387
388        let (_, (used_main, used_aux, _)) =
389            fold.call(fold.init(), (0.0, 0.0, f32::NAN), &childareas);
390
391        let evaluated_area = {
392            let (used_x, used_y) = super::swap_pair(xaxis, (used_main, used_aux));
393            let area = map_unsized_area(myarea, PxDim::new(used_x, used_y));
394
395            // No need to cap this because unsized axis have now been resolved
396            super::limit_area(area * outer_safe, limits)
397        };
398
399        let (unsized_x, unsized_y) = check_unsized_abs(outer_area.bottomright());
400
401        let mut staging: im::Vector<Option<Box<dyn Staged>>> = im::Vector::new();
402        let mut nodes: im::Vector<Option<Rc<rtree::Node>>> = im::Vector::new();
403
404        if (unsized_x && xaxis) || (unsized_y && !xaxis) {
405            // If we are evaluating our staged area along the main axis, no further calculations can be done
406            return Box::new(Concrete {
407                area: evaluated_area,
408                renderable: None,
409                rtree: rtree::Node::new(
410                    evaluated_area.to_untyped(),
411                    Some(props.zindex()),
412                    nodes,
413                    id,
414                    window,
415                ),
416                children: staging,
417                layer: None,
418            });
419        }
420
421        let (total_main, total_aux) = inner_dim.swap_axis(xaxis);
422        // If we need to do wrapping, we do this first, before calculating anything else.
423        let (breaks, linecount, used_aux) = if props.wrap() {
424            // Anything other than `start` for main-axis justification causes problems if there are any obstacles we need to
425            // flow around. To make our first wrapping guess, we simply assume there is only one line when choosing our starting location.
426
427            let r = wrap_line(
428                &childareas,
429                props,
430                xaxis,
431                total_main,
432                total_aux,
433                used_aux,
434                1,
435                props.align(),
436                props.direction() == RowDirection::BottomToTop
437                    || props.direction() == RowDirection::RightToLeft,
438                window.dpi,
439            );
440
441            if !props.obstacles().is_empty() && props.align() != FlexJustify::Start {
442                // If there were obstacles and multiple rows, our initial guess was probably wrong, so rewrap until we converge
443                let mut used_aux = used_aux;
444                let mut prev = (SmallVec::new(), 1, used_aux);
445                let mut linecount = 0;
446
447                // Given the linecount and how we are arranging the rows, figure out the correct initial height
448                while linecount != prev.1 || (used_aux - prev.2) > 0.001 {
449                    linecount = prev.1;
450                    used_aux = prev.2;
451                    prev = wrap_line(
452                        &childareas,
453                        props,
454                        xaxis,
455                        total_main,
456                        total_aux,
457                        prev.2,
458                        prev.1,
459                        props.align(),
460                        props.direction() == RowDirection::BottomToTop
461                            || props.direction() == RowDirection::RightToLeft,
462                        window.dpi,
463                    );
464                }
465
466                prev
467            } else {
468                (r.0, r.1, used_aux)
469            }
470        } else {
471            let mut breaks = SmallVec::<[Linebreak; 10]>::new();
472            breaks.push(Linebreak::new(
473                childareas.len(),
474                total_aux,
475                f32::INFINITY,
476                f32::NAN,
477            ));
478            (breaks, 1, used_aux)
479        };
480
481        // Now we calculate the outer spacing (at the start and end) vs the inner spacing.
482        let (mut aux, inner_aux) =
483            justify_inner_outer(props.align(), total_aux, used_aux, linecount);
484
485        let mut main = 0.0;
486        let mut curindex = 0;
487        let mut prev_margin = f32::NAN;
488
489        // Now we go through each line and apply flex sizing along the main axis.
490        for indice in 0..breaks.len() {
491            let b = &breaks[indice];
492            let mut totalgrow = 0.0;
493            let mut totalshrink = 0.0;
494            let mut used = 0.0;
495
496            // Gather the total basis, grow and shrink values
497            for i in curindex..b.index {
498                let Some(a) = &childareas[i] else {
499                    continue;
500                };
501                totalgrow += a.grow;
502                totalshrink += a.shrink;
503                used += a.basis;
504            }
505
506            // Get the total length of this span, and if necessary, find the line height by scanning ahead.
507            let (total_span, max_aux) = if b.skip.is_finite() {
508                let mut max_aux = breaks.last().unwrap().lineheight;
509                for j in indice..breaks.len() {
510                    if breaks[j].skip.is_infinite() {
511                        max_aux = breaks[j].lineheight;
512                        break;
513                    }
514                }
515
516                (b.skip - main, max_aux)
517            } else {
518                (total_main - main, b.lineheight)
519            };
520
521            let diff = total_span - used;
522            let ratio = if diff > 0.0 {
523                if totalgrow != 0.0 {
524                    diff / totalgrow
525                } else {
526                    0.0
527                }
528            } else if totalshrink != 0.0 {
529                diff / totalshrink
530            } else {
531                0.0
532            };
533
534            for i in curindex..b.index {
535                if let Some(child) = &mut childareas[i] {
536                    child.basis += ratio * (if diff > 0.0 { child.grow } else { child.shrink });
537                }
538            }
539
540            let (outer, inner) = justify_inner_outer(
541                props.justify(),
542                total_span,
543                used,
544                (b.index - curindex) as i32,
545            );
546            main += outer;
547
548            // Construct the final area rectangle for each child
549            for i in curindex..b.index {
550                let Some(c) = &childareas[i] else {
551                    continue;
552                };
553
554                // Apply our margin first
555                if !prev_margin.is_nan() {
556                    main += prev_margin.max(c.margin.topleft().width);
557                }
558                prev_margin = c.margin.bottomright().width;
559
560                // If we're growing backwards, we flip along the main axis (but not the aux axis)
561                let mut area = if props.direction() == RowDirection::RightToLeft
562                    || props.direction() == RowDirection::BottomToTop
563                {
564                    PxRect::new(
565                        total_main - main,
566                        aux,
567                        total_main - (main + c.basis),
568                        aux + max_aux,
569                    )
570                } else {
571                    PxRect::new(main, aux, main + c.basis, aux + max_aux)
572                };
573
574                super::assert_sized(area);
575                area.set_topleft(area.topleft().min(area.bottomright()));
576                // If our axis is swapped, swap the rectangle axis
577                if !xaxis {
578                    let ltrb = area.v.as_array_mut();
579                    ltrb.swap(0, 1);
580                    ltrb.swap(2, 3);
581                }
582
583                let stage = children[i]
584                    .as_ref()
585                    .unwrap()
586                    .stage(area, c.limits + limits, window);
587                if let Some(node) = stage.get_rtree().upgrade() {
588                    nodes.push_back(Some(node));
589                }
590                staging.push_back(Some(stage));
591
592                main += c.basis + inner;
593            }
594
595            if b.skip.is_finite() {
596                main = b.lineheight;
597            } else {
598                main = 0.0;
599                aux += b.lineheight + inner_aux;
600
601                if !b.aux_margin.is_nan() {
602                    aux += b.aux_margin;
603                }
604            }
605            prev_margin = f32::NAN;
606            curindex = b.index;
607        }
608
609        Box::new(Concrete {
610            area: evaluated_area,
611            renderable,
612            rtree: rtree::Node::new(
613                evaluated_area.to_untyped(),
614                Some(props.zindex()),
615                nodes,
616                id,
617                window,
618            ),
619            children: staging,
620            layer: None,
621        })
622    }
623}