Skip to main content

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