cotis-layout 0.1.0-alpha

Flexbox-style layout engine for Cotis
Documentation
use crate::layout_struct::layout_states::element_info_states::TotalMinSize;
use crate::layout_struct::layout_states::{
    ChildWrapping, LayoutElement, LayoutElementInfo, element_info_states,
};
use crate::layout_struct::layout_tree::LayoutTreeNodeLocal;
use cotis_defaults::element_configs::style::types::LayoutDirection;

/// Vertical min/preferred height from padding + in-flow children (same height rules as
/// [`LayoutTreeNodeLocal::close_element`]), merged with this node's sizing (`calc_start_dim`, clip,
/// declared max on the height axis).
pub(crate) fn merged_vertical_bounds_from_children(
    element: &LayoutElement,
    children: &[&LayoutElement],
) -> (f32, f32) {
    let mut total_min_height =
        element.config.layout.padding.top + element.config.layout.padding.bottom;
    let mut total_pref_height =
        element.config.layout.padding.top + element.config.layout.padding.bottom;

    match element.config.layout.layout_direction {
        LayoutDirection::LeftToRight => {
            let mut max_min_height = 0.0f32;
            let mut max_pref_height = 0.0f32;
            for child in children {
                if child.config.floating.config.is_some() {
                    continue;
                }
                max_min_height = max_min_height.max(
                    child
                        .info
                        .min_height()
                        .expect("child must expose min height during layout"),
                );
                max_pref_height = max_pref_height.max(
                    child
                        .info
                        .temp_height()
                        .expect("child must expose temp height during layout"),
                );
            }
            total_min_height += max_min_height;
            total_pref_height += max_pref_height;
        }
        LayoutDirection::TopToBottom => {
            for child in children {
                if child.config.floating.config.is_some() {
                    continue;
                }
                total_min_height += child
                    .info
                    .min_height()
                    .expect("child must expose min height during layout");
                total_pref_height += child
                    .info
                    .temp_height()
                    .expect("child must expose temp height during layout");
            }
            total_min_height += element.config.layout.child_gap * children.len().max(1) as f32;
        }
    }

    let node_mins = element.calc_start_dim().into_total_min_size();
    let raw_pref_h = node_mins.preferred_size_height.max(total_pref_height);
    let merged_min_h = node_mins
        .min_size_height
        .max(if element.config.clip.vertical {
            0.0
        } else {
            total_min_height
        });
    let merged_pref_h = if element.config.clip.vertical {
        raw_pref_h
    } else {
        raw_pref_h
    };
    (merged_min_h, merged_pref_h)
}

impl LayoutTreeNodeLocal<'_> {
    pub(crate) fn recalculate_min_height_from_children(&mut self) {
        let (merged_min_h, merged_pref_h) =
            merged_vertical_bounds_from_children(self.self_element, &self.children_elements);
        match &self.self_element.info {
            LayoutElementInfo::NotInitialized
            | LayoutElementInfo::TotalSize(_)
            | LayoutElementInfo::TotalSizedAndPosition(_) => {}
            _ => {
                self.self_element.info.set_min_height(merged_min_h);
                self.self_element.info.set_tmp_height(merged_pref_h);
            }
        }
    }

    pub fn open_configuration(&mut self) {
        // Setup z_index
        if self.self_element.config.floating.config.is_none() {
            self.self_element.config.floating.z_index = self
                .parent_element
                .map(|p| p.config.floating.z_index)
                .unwrap_or(0);
        }
    }

    pub fn close_element(&mut self) {
        let mut total_min_size: element_info_states::TotalMinSize = TotalMinSize {
            min_size_width: self.self_element.config.layout.padding.left
                + self.self_element.config.layout.padding.right,
            min_size_height: self.self_element.config.layout.padding.top
                + self.self_element.config.layout.padding.bottom,
            preferred_size_width: self.self_element.config.layout.padding.left
                + self.self_element.config.layout.padding.right,
            preferred_size_height: self.self_element.config.layout.padding.top
                + self.self_element.config.layout.padding.bottom,
        };
        match self.self_element.config.layout.layout_direction {
            LayoutDirection::LeftToRight => {
                let mut max_min_height = 0.0;
                let mut max_pref_height = 0.0;
                let wrap = self.self_element.config.layout.wrapping == ChildWrapping::Text;
                let mut max_min_child_width = 0.0f32;
                for child in &self.children_elements {
                    if let LayoutElementInfo::TotalMinSize(child_min_size) = &child.info {
                        if wrap {
                            max_min_child_width =
                                max_min_child_width.max(child_min_size.min_size_width);
                        } else {
                            total_min_size.min_size_width += child_min_size.min_size_width;
                        }
                        max_min_height = child_min_size.min_size_height.max(max_min_height);
                        total_min_size.preferred_size_width += child_min_size.preferred_size_width;
                        max_pref_height = child_min_size.preferred_size_height.max(max_pref_height);
                    } else {
                        panic!(
                            "How did we get here. Trying to calculate total min size without child total min size"
                        )
                    }
                }
                total_min_size.min_size_height += max_min_height;
                total_min_size.preferred_size_height += max_pref_height;
                if wrap {
                    total_min_size.min_size_width += max_min_child_width;
                    // In text-wrap rows, children are words/spacers and horizontal spacing is
                    // represented by `child_gap`. Preferred width must include that gap budget,
                    // otherwise container preferred width is undersized and words wrap too early.
                    total_min_size.preferred_size_width +=
                        self.self_element.config.layout.child_gap
                            * self.children_elements.len().saturating_sub(1) as f32;
                } else {
                    total_min_size.min_size_width += self.self_element.config.layout.child_gap
                        as f32
                        * self.children_elements.len().max(1) as f32;
                }
            }
            LayoutDirection::TopToBottom => {
                let mut max_min_width = 0.0;
                let mut max_pref_width = 0.0;
                for child in &self.children_elements {
                    if let LayoutElementInfo::TotalMinSize(child_min_size) = &child.info {
                        total_min_size.min_size_height += child_min_size.min_size_height;
                        max_min_width = child_min_size.min_size_width.max(max_min_width);
                        total_min_size.preferred_size_height +=
                            child_min_size.preferred_size_height;
                        max_pref_width = child_min_size.preferred_size_width.max(max_pref_width);
                    } else {
                        panic!(
                            "How did we get here. Trying to calculate total min size without child total min size"
                        )
                    }
                }
                total_min_size.min_size_width += max_min_width;
                total_min_size.preferred_size_width += max_pref_width;
                total_min_size.min_size_height += self.self_element.config.layout.child_gap as f32
                    * self.children_elements.len().max(1) as f32;
            }
        }
        let node_mins = self.self_element.calc_start_dim().into_total_min_size();
        let raw_pref_w = node_mins
            .preferred_size_width
            .max(total_min_size.preferred_size_width);
        let raw_pref_h = node_mins
            .preferred_size_height
            .max(total_min_size.preferred_size_height);
        let max_w = self.self_element.declared_max_axis(true);
        let max_h = self.self_element.declared_max_axis(false);
        let total_min_size = TotalMinSize {
            min_size_width: node_mins.min_size_width.max(
                if self.self_element.config.clip.horizontal {
                    0.0
                } else {
                    total_min_size.min_size_width.min(max_w)
                },
            ),
            min_size_height: node_mins.min_size_height.max(
                if self.self_element.config.clip.vertical {
                    0.0
                } else {
                    total_min_size.min_size_height.min(max_h)
                },
            ),
            // Clipping allows overflow in child layout, but the element's own box must still
            // honor its declared sizing bounds (e.g. Fixed / Fit / Grow max).
            preferred_size_width: raw_pref_w.min(max_w),
            preferred_size_height: raw_pref_h.min(max_h),
        };
        self.self_element.info = LayoutElementInfo::TotalMinSize(total_min_size);
    }
}