repose-ui 0.26.1

UI widgets and libs for Repose
Documentation
#![allow(non_snake_case)]

use std::collections::HashMap;

use repose_core::*;
use repose_tree::{NodeId, ViewTree};
use rustc_hash::FxHashMap;
use taffy::TaffyTree;
use taffy::prelude::*;

use crate::textfield::{TextMeasureConfig, measure_text};

use super::*;

impl LayoutEngine {
    pub(crate) fn run_measure_pass(
        taffy: &mut TaffyTree<NodeContext>,
        taffy_root: taffy::NodeId,
        available: taffy::geometry::Size<taffy::style::AvailableSpace>,
        tree: &ViewTree,
        text_cache: &mut FxHashMap<NodeId, TextLayout>,
        reverse_map: &FxHashMap<taffy::NodeId, NodeId>,
        scope_root_map: &FxHashMap<NodeId, String>,
        node_to_scope: &FxHashMap<NodeId, String>,
        scope_trees: &mut HashMap<String, ScopeLayoutTree>,
        font_px: &dyn Fn(f32) -> f32,
        px: &dyn Fn(f32) -> f32,
    ) {
        let _ = taffy.compute_layout_with_measure(
            taffy_root,
            available,
            |known, avail, taffy_node, ctx, _style| {
                // Check if this is a scope root marker → return cached scope size
                if let Some(&node_id) = reverse_map.get(&taffy_node) {
                    // Custom layout modifier: delegate measurement to user callback
                    if let Some(node) = tree.get(node_id) {
                        if let Some(ref layout_cb) = node.modifier.layout {
                            let scale = dp_to_px(1.0);
                            let avail_w = match avail.width {
                                AvailableSpace::Definite(w) => w / scale,
                                _ => f32::INFINITY,
                            };
                            let avail_h = match avail.height {
                                AvailableSpace::Definite(h) => h / scale,
                                _ => f32::INFINITY,
                            };
                            let known_w =
                                known.width.map(|w| w / scale).unwrap_or(f32::INFINITY);
                            let known_h = known
                                .height
                                .map(|h| h / scale)
                                .unwrap_or(f32::INFINITY);
                            let constraints =
                                repose_core::modifier::LayoutConstraints {
                                    min_width: 0.0,
                                    max_width: avail_w.min(known_w),
                                    min_height: 0.0,
                                    max_height: avail_h.min(known_h),
                                };
                            let (w_dp, h_dp) = layout_cb(constraints);
                            return taffy::geometry::Size {
                                width: w_dp * scale,
                                height: h_dp * scale,
                            };
                        }
                    }
                    if scope_root_map.contains_key(&node_id) {
                        if let Some(key) = node_to_scope.get(&node_id) {
                            if let Some(st) = scope_trees.get_mut(key) {
                                // Compose-style constraint-equality skip:
                                // if content unchanged (valid) AND constraints match → skip scope compute
                                let constraints_changed = st
                                    .last_constraints
                                    .map(|(k, a)| k != known || a != avail)
                                    .unwrap_or(true);
                                let can_skip = st.valid && !constraints_changed;
                                if can_skip {
                                    if let Some(sz) = st.cached_size {
                                        return sz;
                                    }
                                }
                                // Compute scope's internal layout on-demand
                                if let Some(root_tid) = st.root_taffy_id {
                                    let scope_avail = taffy::geometry::Size {
                                        width: match known.width {
                                            Some(w) if w.is_finite() => {
                                                AvailableSpace::Definite(w)
                                            }
                                            _ => match avail.width {
                                                AvailableSpace::Definite(w) => {
                                                    AvailableSpace::Definite(w)
                                                }
                                                _ => AvailableSpace::MaxContent,
                                            },
                                        },
                                        height: match known.height {
                                            Some(h) if h.is_finite() => {
                                                AvailableSpace::Definite(h)
                                            }
                                            _ => match avail.height {
                                                AvailableSpace::Definite(h) => {
                                                    AvailableSpace::Definite(h)
                                                }
                                                _ => AvailableSpace::MaxContent,
                                            },
                                        },
                                    };
                                    let st_rev = &st.reverse_map;
                                    let st_tc = &mut st.text_cache;
                                    let _ = st.taffy.compute_layout_with_measure(
                                        root_tid,
                                        scope_avail,
                                        |known2, avail2, tn, ctx2, _style2| {
                                            Self::measure_node(
                                                known2,
                                                avail2,
                                                tn,
                                                ctx2.as_deref(),
                                                st_tc,
                                                st_rev,
                                                tree,
                                                font_px,
                                                px,
                                            )
                                        },
                                    );
                                    st.last_constraints = Some((known, avail));
                                    if let Ok(layout) = st.taffy.layout(root_tid) {
                                        st.cached_size = Some(taffy::Size {
                                            width: layout.size.width,
                                            height: layout.size.height,
                                        });
                                        st.valid = true;
                                        return st.cached_size.unwrap();
                                    }
                                }
                            }
                        }
                    }
                }
                Self::measure_node(
                    known,
                    avail,
                    taffy_node,
                    ctx.as_deref(),
                    text_cache,
                    reverse_map,
                    tree,
                    font_px,
                    px,
                )
            },
        );
    }

    pub(crate) fn measure_node(
        known: taffy::geometry::Size<Option<f32>>,
        avail: taffy::geometry::Size<AvailableSpace>,
        taffy_node: taffy::NodeId,
        ctx: Option<&NodeContext>,
        text_cache: &mut FxHashMap<NodeId, TextLayout>,
        reverse_map: &FxHashMap<taffy::NodeId, NodeId>,
        _tree: &ViewTree,
        font_px: &dyn Fn(f32) -> f32,
        px: &dyn Fn(f32) -> f32,
    ) -> taffy::geometry::Size<f32> {
        match ctx {
            Some(NodeContext::Text {
                text,
                color: _,
                font_dp,
                soft_wrap,
                max_lines,
                overflow,
                font_family,
                annotations: _,
                text_align: _,
                font_weight,
                font_style,
                text_decoration: _,
                letter_spacing,
                line_height,
                font_variation_settings,
            }) => {
                let size_px_val = font_px(*font_dp);
                let lh = if *line_height > 0.0 {
                    font_px(*line_height)
                } else {
                    size_px_val
                };
                let line_h_px_val = lh;
                let fw = font_weight.0;
                let fs = if matches!(font_style, FontStyle::Italic) {
                    1
                } else {
                    0
                };
                let max_content_w = measure_text(text, size_px_val, TextMeasureConfig { font_family: *font_family, font_weight: fw, font_style: fs, letter_spacing: *letter_spacing, ..Default::default() })
                    .positions
                    .last()
                    .copied()
                    .unwrap_or(0.0)
                    .max(0.0);

                let mut min_content_w = 0.0f32;
                for w in text.split_whitespace() {
                    let ww = measure_text(w, size_px_val, TextMeasureConfig { font_family: *font_family, font_weight: fw, font_style: fs, letter_spacing: *letter_spacing, ..Default::default() })
                        .positions
                        .last()
                        .copied()
                        .unwrap_or(0.0);
                    min_content_w = min_content_w.max(ww);
                }
                if min_content_w <= 0.0 {
                    min_content_w = max_content_w;
                }

                let wrap_w_px = if let Some(w) = known.width.filter(|w| *w > 0.5) {
                    w
                } else {
                    match avail.width {
                        AvailableSpace::Definite(w) if w > 0.5 => w,
                        AvailableSpace::MinContent => min_content_w,
                        AvailableSpace::MaxContent => max_content_w,
                        _ => max_content_w,
                    }
                };

                let fvs = font_variation_settings.as_deref();
                let (lines, line_ranges): (Vec<String>, Vec<(usize, usize)>) = if *soft_wrap {
                    let (ranges, truncated) = repose_text::wrap_line_ranges(
                        text,
                        size_px_val,
                        wrap_w_px,
                        *max_lines,
                        true,
                        fw,
                        fs,
                        *letter_spacing,
                        fvs,
                    );
                    let mut lns: Vec<String> = ranges
                        .iter()
                        .map(|&(s, e)| text[s..e].to_string())
                        .collect();
                    if truncated && matches!(overflow, TextOverflow::Ellipsis) {
                        if let Some(last) = lns.last_mut() {
                            let with_tail = format!("{}", last);
                            *last =
                                repose_text::ellipsize_line(&with_tail, size_px_val, wrap_w_px, fw, fs, *letter_spacing, fvs);
                        }
                    }
                    (lns, ranges)
                } else if matches!(overflow, TextOverflow::Ellipsis) {
                    let elided = repose_text::ellipsize_line(text, size_px_val, wrap_w_px, fw, fs, *letter_spacing, fvs);
                    let elided_len = elided.len();
                    (vec![elided], vec![(0, elided_len)])
                } else {
                    let len = text.len();
                    (vec![text.clone()], vec![(0, len)])
                };

                let line_widths: Vec<f32> = lines
                    .iter()
                    .map(|line| {
                        measure_text(line, size_px_val, TextMeasureConfig { font_family: *font_family, font_weight: fw, font_style: fs, letter_spacing: *letter_spacing, ..Default::default() })
                            .positions
                            .last()
                            .copied()
                            .unwrap_or(0.0)
                    })
                    .collect();
                let max_line_w = line_widths.iter().copied().fold(0.0f32, f32::max);

                if let Some(node_id) = reverse_map.get(&taffy_node) {
                    text_cache.insert(
                        *node_id,
                        TextLayout {
                            lines: lines.clone(),
                            line_ranges,
                            size_px: size_px_val,
                            line_h_px: line_h_px_val,
                            line_widths,
                        },
                    );
                }
                taffy::geometry::Size {
                    width: max_line_w,
                    height: line_h_px_val * lines.len().max(1) as f32,
                }
            }
            Some(NodeContext::TextInput { multiline }) => {
                let natural_w = px(160.0);
                let width = match avail.width {
                    AvailableSpace::Definite(w) if w > 0.5 => w,
                    AvailableSpace::MinContent => px(48.0).max(natural_w),
                    _ => known.width.unwrap_or(natural_w),
                };
                let natural_h = if *multiline { px(140.0) } else { px(36.0) };
                let height = match avail.height {
                    AvailableSpace::Definite(h) if h > 0.5 => h,
                    AvailableSpace::MinContent => px(28.0).max(natural_h),
                    _ => known.height.unwrap_or(natural_h),
                };
                taffy::geometry::Size { width, height }
            }
            _ => taffy::geometry::Size::ZERO,
        }
    }
}