feather-ui 0.4.0

Feather UI library
Documentation
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2025 Fundament Research Institute <https://fundament.institute>

use super::{
    Concrete, Desc, Layout, Renderable, Staged, base, check_unsized_abs, map_unsized_area,
    nuetralize_unsized,
};
use crate::{PxDim, PxPoint, PxRect, RowDirection, SourceID, rtree};
use std::rc::Rc;

pub trait Prop: base::Area + base::Limits + base::Direction + base::Anchor {}

crate::gen_from_to_dyn!(Prop);

pub trait Child: base::RLimits + base::Margin + base::Order {}

crate::gen_from_to_dyn!(Child);

impl Desc for dyn Prop {
    type Props = dyn Prop;
    type Child = dyn Child;
    // TODO: Make a sorted im::Vector that uses base::Order to order inserted
    // children.
    type Children = im::Vector<Option<Box<dyn Layout<Self::Child>>>>;

    fn stage<'a>(
        props: &Self::Props,
        outer_area: PxRect,
        outer_limits: crate::PxLimits,
        children: &Self::Children,
        id: std::sync::Weak<SourceID>,
        renderable: Option<Rc<dyn Renderable>>,
        window: &mut crate::component::window::WindowState,
    ) -> Box<dyn Staged + 'a> {
        use super::Swappable;
        // TODO: make insertion efficient by creating a RRB tree of list layout
        // subnodes, in a similar manner to the r-tree nodes.

        let limits = outer_limits + props.limits().resolve(window.dpi);
        let myarea = props.area().resolve(window.dpi);
        //let (unsized_x, unsized_y) = super::check_unsized(*myarea);
        let dir = props.direction();
        // For the purpose of calculating size, we only care about which axis we're
        // distributing along
        let xaxis = match dir {
            RowDirection::LeftToRight | RowDirection::RightToLeft => true,
            RowDirection::TopToBottom | RowDirection::BottomToTop => false,
        };

        // Even if both axis are sized, we have to precalculate the areas and margins
        // anyway.
        let inner_dim = super::limit_dim(super::eval_dim(myarea, outer_area.dim()), limits);
        let outer_safe = nuetralize_unsized(outer_area);
        // The inner_dim must preserve whether an axis is unsized, but the actual limits
        // must be respected regardless.
        let (main_limit, _) = inner_dim.min(limits.max()).swap_axis(xaxis);

        // This should eventually be a persistent fold
        let mut aux_margins: im::Vector<f32> = im::Vector::new();
        let mut areas: im::Vector<Option<(PxRect, f32)>> = im::Vector::new();

        let area = {
            let mut cur = PxPoint::zero();
            let mut max_main = 0.0;
            let mut max_aux: f32 = 0.0;
            let mut prev_margin = f32::NAN;
            let mut aux_margin: f32 = 0.0;
            let mut aux_margin_bottom = f32::NAN;
            let mut prev_aux_margin = f32::NAN;
            let inner_area = PxRect::from(inner_dim);

            for child in children.iter() {
                let child_props = child.as_ref().unwrap().get_props();
                let child_limit = super::apply_limit(inner_dim, limits, *child_props.rlimits());
                let child_margin = child_props
                    .margin()
                    .resolve(window.dpi)
                    .to_perimeter(outer_safe);

                let stage = child
                    .as_ref()
                    .unwrap()
                    .stage(inner_area, child_limit, window);
                let area = stage.get_area();

                let (margin_main, child_margin_aux) = child_margin.topleft().swap_axis(xaxis);
                let (main, aux) = area.dim().swap_axis(xaxis);
                let mut margin = super::merge_margin(prev_margin, margin_main);
                // Have to add the margin here before we zero it
                areas.push_back(Some((area, margin)));

                if !prev_margin.is_nan() && cur.x + main + margin > main_limit {
                    max_main = cur.x.max(max_main);
                    cur.x = 0.0;
                    let aux_merge = super::merge_margin(prev_aux_margin, aux_margin);
                    aux_margins.push_back(aux_merge);
                    cur.y += max_aux + aux_merge;
                    max_aux = 0.0;
                    margin = 0.0;
                    aux_margin = 0.0;
                    prev_aux_margin = aux_margin_bottom;
                    aux_margin_bottom = f32::NAN;
                }

                cur.x += main + margin;
                aux_margin = aux_margin.max(child_margin_aux);
                max_aux = max_aux.max(aux);

                let (margin, child_margin_aux) = child_margin.bottomright().swap_axis(xaxis);
                prev_margin = margin;
                aux_margin_bottom = aux_margin_bottom.max(child_margin_aux);
            }

            // Final bounds calculations
            max_main = cur.x.max(max_main);
            let aux_merge = super::merge_margin(prev_aux_margin, aux_margin);
            aux_margins.push_back(aux_merge);
            cur.y += max_aux + aux_margin;
            let (bounds_x, bounds_y) = super::swap_pair(xaxis, (max_main, cur.y));
            map_unsized_area(myarea, PxDim::new(bounds_x, bounds_y))
        };

        // No need to cap this because unsized axis have now been resolved
        let evaluated_area = super::limit_area(area * outer_safe, limits);

        let anchor = props.anchor().resolve(window.dpi) * evaluated_area.dim();
        let evaluated_area = evaluated_area - anchor;

        let mut staging: im::Vector<Option<Box<dyn Staged>>> = im::Vector::new();
        let mut nodes: im::Vector<Option<Rc<rtree::Node>>> = im::Vector::new();

        // If our parent is asking for a size estimation along the expansion axis, no
        // need to layout the children TODO: Double check this assumption is
        // true
        let (unsized_x, unsized_y) = check_unsized_abs(outer_area.bottomright());
        if (unsized_x && xaxis) || (unsized_y && !xaxis) {
            debug_assert!(evaluated_area.v.is_finite().all());
            return Box::new(Concrete {
                area: evaluated_area,
                renderable: None,
                rtree: rtree::Node::new(evaluated_area.to_untyped(), None, nodes, id, window),
                children: staging,
                layer: None,
            });
        }

        let evaluated_dim = evaluated_area.dim();
        let mut cur = match dir {
            RowDirection::LeftToRight | RowDirection::TopToBottom => PxPoint::zero(),
            RowDirection::RightToLeft => PxPoint::new(evaluated_dim.width, 0.0),
            RowDirection::BottomToTop => PxPoint::new(0.0, evaluated_dim.height),
        };
        let mut maxaux: f32 = 0.0;
        aux_margins.pop_front();
        let mut aux_margin = aux_margins.pop_front().unwrap_or_default();

        for (i, child) in children.iter().enumerate() {
            let child = child.as_ref().unwrap();
            let (area, margin) = areas[i].unwrap();
            let dim = area.dim();
            let (_, aux) = dim.swap_axis(xaxis);

            match dir {
                RowDirection::RightToLeft => {
                    if cur.x - dim.width - margin < 0.0 {
                        cur.y += maxaux + aux_margin;
                        aux_margin = aux_margins.pop_front().unwrap_or_default();
                        maxaux = 0.0;
                        cur.x = evaluated_dim.width - dim.width;
                    } else {
                        cur.x -= dim.width + margin
                    }
                }
                RowDirection::BottomToTop => {
                    if cur.y - dim.height - margin < 0.0 {
                        cur.x += maxaux + aux_margin;
                        aux_margin = aux_margins.pop_front().unwrap_or_default();
                        maxaux = 0.0;
                        cur.y = evaluated_dim.height - dim.height;
                    } else {
                        cur.y -= dim.height + margin
                    }
                }
                RowDirection::LeftToRight => {
                    if cur.x + dim.width + margin > evaluated_dim.width {
                        cur.y += maxaux + aux_margin;
                        aux_margin = aux_margins.pop_front().unwrap_or_default();
                        maxaux = 0.0;
                        cur.x = 0.0;
                    } else {
                        cur.x += margin;
                    }
                }
                RowDirection::TopToBottom => {
                    if cur.y + dim.height + margin > evaluated_dim.height {
                        cur.x += maxaux + aux_margin;
                        aux_margin = aux_margins.pop_front().unwrap_or_default();
                        maxaux = 0.0;
                        cur.y = 0.0;
                    } else {
                        cur.y += margin;
                    }
                }
            };

            let child_area = area + cur;

            match dir {
                RowDirection::LeftToRight => cur.x += dim.width,
                RowDirection::TopToBottom => cur.y += dim.height,
                _ => (),
            };
            maxaux = maxaux.max(aux);

            let child_limit = *child.get_props().rlimits() * evaluated_dim;

            let stage = child.stage(child_area, child_limit, window);
            if let Some(node) = stage.get_rtree().upgrade() {
                nodes.push_back(Some(node));
            }
            staging.push_back(Some(stage));
        }

        debug_assert!(evaluated_area.v.is_finite().all());
        Box::new(Concrete {
            area: evaluated_area,
            renderable,
            rtree: rtree::Node::new(evaluated_area.to_untyped(), None, nodes, id, window),
            children: staging,
            layer: None,
        })
    }
}