cotis-layout 0.1.0-alpha.1

Flexbox-style layout engine for Cotis
Documentation
use std::sync::Arc;
use cotis::utils::ElementIdConfig;
use cotis_defaults::colors::Color;
use cotis_defaults::element_configs::ElementConfig;
use cotis_defaults::element_configs::style::{BorderElementStyle, BorderWidthStyle};
use cotis_defaults::element_configs::style::sizing::{AxisSizing, DoubleAxisSizing, Sizing};
use cotis_defaults::element_configs::style::types::{LayoutDirection, Padding};
use cotis_defaults::element_configs::text_config::{TextElementConfig, TextElementConfigWrapMode};
use cotis_utils::math::Dimensions;
use crate::layout_management::CotisLayoutRun;
use crate::layout_struct::layout_states::{ChildWrapping, LayoutElementConfig};
use crate::layout_struct::TextDrawPayload;
use crate::text::{line_word_space_pieces, measure_text_fragment, newline_line_ranges, text_line_child_specs, TextLineChildSpec};

/// When `intrinsic_fit` is true, uses `AxisSizing::Fit` (min 0, max measured) so the node can shrink when the parent is tight.
fn synthetic_text_child_element_config<'frame, I, C, E>(
    parent: &ElementConfig<'frame, Sizing, I, C, E>,
    layout_direction: LayoutDirection,
    child_gap: f32,
    measured: Dimensions,
    intrinsic_fit: bool,
) -> ElementConfig<'frame, Sizing, I, C, E> {
    #[cfg(any(feature = "complex_color", feature = "complex_colored_text"))]
    let mut style = parent.style.clone();
    #[cfg(not(any(feature = "complex_color", feature = "complex_colored_text")))]
    let mut style = parent.style;
    #[cfg(any(feature = "complex_color", feature = "complex_colored_text"))]
    {
        style.background_color = ColorLayer::Solid(Color::rgba(0.0, 0.0, 0.0, 0.0));
    }
    #[cfg(not(any(feature = "complex_color", feature = "complex_colored_text")))]
    {
        style.background_color.a = 0.0;
    }
    style.layout.layout_direction = layout_direction;
    style.layout.child_gap = child_gap;
    style.layout.padding = Padding {
        top: 0.0,
        bottom: 0.0,
        left: 0.0,
        right: 0.0,
    };
    let w = measured.width.max(0.0);
    let h = measured.height.max(0.0);
    style.sizing = Sizing::DoubleAxis(DoubleAxisSizing {
        width: if intrinsic_fit {
            AxisSizing::Fit(0.0, w)
        } else {
            AxisSizing::Fixed(w)
        },
        height: if intrinsic_fit {
            AxisSizing::Fit(h, f32::MAX)
        } else {
            AxisSizing::Fixed(h)
        },
    });
    style.floating = None;
    style.border = BorderElementStyle {
        color: Color {
            r: 0.0,
            g: 0.0,
            b: 0.0,
            a: 0.0,
        },
        width: BorderWidthStyle {
            left: 0.0,
            right: 0.0,
            top: 0.0,
            bottom: 0.0,
            between_children: 0.0,
        },
    };
    ElementConfig {
        id_config: ElementIdConfig::new_empty(),
        style,
        image: None,
        custom: None,
        extra_data: None,
    }
}

impl<'layout, 'frame, I, C, E> CotisLayoutRun<'layout, ElementConfig<'frame, I, C, E>> {
    pub(crate) fn apply_text_leaf(&mut self, leaf: TextElementConfig<'frame>) {
        let container_id = self.open_element_id();
        let source: Arc<str> = Arc::from(leaf.text.as_ref());
        #[cfg(feature = "complex_colored_text")]
        let text_style = leaf.config.clone();
        #[cfg(not(feature = "complex_colored_text"))]
        let text_style = leaf.config;
        {
            let mut node = self
                .layout_manager
                .root
                .get_node_mut(&self.open_element_index)
                .expect("[Cotis Layout] text leaf open element missing");
            node.get_local().self_element.config.layout.layout_direction = LayoutDirection::TopToBottom;
        }

        let word_row_gap = match text_style.wrap_mode {
            TextElementConfigWrapMode::Words => measure_text_fragment(
                " ",
                &text_style,
                self.layout_manager.text_dim_fun.as_ref(),
            ).width,
            _ => 0.0,
        };

        for line_range in newline_line_ranges(source.as_ref()) {
            let line_slice_full = &source[line_range.start..line_range.end];
            let (line_dims, words_specs) = match text_style.wrap_mode {
                TextElementConfigWrapMode::Words => {
                    let empty_dims = measure_text_fragment(
                        "",
                        &text_style,
                        self.layout_manager.text_dim_fun.as_ref(),
                    );
                    let pieces = line_word_space_pieces(line_slice_full);
                    let specs = text_line_child_specs(&pieces);
                    let dims = if specs.is_empty() {
                        empty_dims
                    } else {
                        let mut sum_w = 0.0f32;
                        let mut max_h = empty_dims.height;
                        for spec in &specs {
                            if let TextLineChildSpec::Word(r) = spec {
                                let fragment = &line_slice_full[r.clone()];
                                let d = measure_text_fragment(
                                    fragment,
                                    &text_style,
                                    self.layout_manager.text_dim_fun.as_ref(),
                                );
                                sum_w += d.width;
                                max_h = max_h.max(d.height);
                            }
                        }
                        let gap_total = (specs.len().saturating_sub(1)) as f32 * word_row_gap;
                        Dimensions {
                            width: sum_w + gap_total,
                            height: max_h,
                        }
                    };
                    (dims, specs)
                }
                TextElementConfigWrapMode::Newline | TextElementConfigWrapMode::None => {
                    let dims = measure_text_fragment(
                        line_slice_full,
                        &text_style,
                        self.layout_manager.text_dim_fun.as_ref(),
                    );
                    (dims, Vec::new())
                }
            };

            self.new_child_element();
            let mut line_el = {
                let parent_cfg = self
                    .configs
                    .get(&container_id)
                    .expect("[Cotis Layout] text leaf needs set_config before set_leaf_config");
                synthetic_text_child_element_config(
                    parent_cfg,
                    LayoutDirection::LeftToRight,
                    word_row_gap,
                    line_dims,
                    true,
                )
            };
            line_el.id_config.change_id(self.open_element_id());
            let mut line_layout: LayoutElementConfig = (&line_el.style).into();
            line_layout.layout.wrapping = ChildWrapping::Text;
            self.set_open_element_config(line_layout, None, None, line_el);

            match text_style.wrap_mode {
                TextElementConfigWrapMode::Words => {
                    let spacer_measured = Dimensions {
                        width: 0.0,
                        height: line_dims.height,
                    };
                    for spec in words_specs {
                        match spec {
                            TextLineChildSpec::Spacer => {
                                self.new_child_element();
                                let mut spacer_el = {
                                    let parent_cfg = self
                                        .configs
                                        .get(&container_id)
                                        .expect("[Cotis Layout] text leaf needs set_config before set_leaf_config");
                                    synthetic_text_child_element_config(
                                        parent_cfg,
                                        LayoutDirection::LeftToRight,
                                        0.0,
                                        spacer_measured,
                                        false,
                                    )
                                };
                                spacer_el.id_config.change_id(self.open_element_id());
                                let spacer_layout: LayoutElementConfig = (&spacer_el.style).into();
                                self.set_open_element_config(spacer_layout, None, None, spacer_el);
                                self.close_open_element();
                            }
                            TextLineChildSpec::Word(w) => {
                                let global = line_range.start + w.start..line_range.start + w.end;
                                let word_text = &source[global.clone()];
                                let word_measured = measure_text_fragment(
                                    word_text,
                                    &text_style,
                                    self.layout_manager.text_dim_fun.as_ref(),
                                );
                                self.new_child_element();
                                let mut word_el = {
                                    let parent_cfg = self
                                        .configs
                                        .get(&container_id)
                                        .expect("[Cotis Layout] text leaf needs set_config before set_leaf_config");
                                    synthetic_text_child_element_config(
                                        parent_cfg,
                                        LayoutDirection::LeftToRight,
                                        0.0,
                                        word_measured,
                                        false,
                                    )
                                };
                                word_el.id_config.change_id(self.open_element_id());
                                let word_layout: LayoutElementConfig = (&word_el.style).into();
                                self.set_open_element_config(word_layout, None, None, word_el);
                                let wid = self.open_element_id();
                                self.text_fragments.insert(
                                    wid,
                                    TextDrawPayload {
                                        source: Arc::clone(&source),
                                        range: global,
                                        #[cfg(feature = "complex_colored_text")]
                                        config: text_style.clone(),
                                        #[cfg(not(feature = "complex_colored_text"))]
                                        config: text_style,
                                    },
                                );
                                self.close_open_element();
                            }
                        }
                    }
                }
                TextElementConfigWrapMode::Newline | TextElementConfigWrapMode::None => {
                    // One measured in-flow child so `ChildWrapping::Text` close_element gets real width (same as `Words`).
                    if !line_range.is_empty() {
                        let global = line_range.clone();
                        let word_text = &source[global.clone()];
                        let word_measured = measure_text_fragment(
                            word_text,
                            &text_style,
                            self.layout_manager.text_dim_fun.as_ref(),
                        );
                        self.new_child_element();
                        let mut word_el = {
                            let parent_cfg = self
                                .configs
                                .get(&container_id)
                                .expect("[Cotis Layout] text leaf needs set_config before set_leaf_config");
                            synthetic_text_child_element_config(
                                parent_cfg,
                                LayoutDirection::LeftToRight,
                                0.0,
                                word_measured,
                                false,
                            )
                        };
                        word_el.id_config.change_id(self.open_element_id());
                        let word_layout: LayoutElementConfig = (&word_el.style).into();
                        self.set_open_element_config(word_layout, None, None, word_el);
                        let wid = self.open_element_id();
                        self.text_fragments.insert(
                            wid,
                            TextDrawPayload {
                                source: Arc::clone(&source),
                                range: global,
                                #[cfg(feature = "complex_colored_text")]
                                config: text_style.clone(),
                                #[cfg(not(feature = "complex_colored_text"))]
                                config: text_style,
                            },
                        );
                        self.close_open_element();
                    }
                }
            }
            self.close_open_element();
        }
    }
}