//! 原生布局求解器:自底向上测量期望尺寸,再自顶向下分配位置和实际尺寸。 //! Row/Column/Overlay、边距、边框、内边距、对齐和弹性空间都在有限约束内计算;过大的尺寸和坐标返回错误。 //! 缓存按组件形状、子项尺寸和约束判断可复用性。frame 先在克隆状态中求解,成功才替换运行状态,防止失败污染已发布几何。 //! //! 调用路径:UiBridge/原生视觉入口调用 LayoutRuntime 或 frame_with_viewport 进行纯原生布局。 //! source_layout 复用约束、measure_native、flex_sizes、arrange_native 等单节点函数,在外层驱动源协议收敛。 //! 本模块只处理不可变 UiNode 和原生数值,不执行 VM 或持有托管根;每次出现身份由父 ID/slot/业务身份共同确定。 //! //! Iterative measure/arrange/paint over derived visual data. Every occurrence //! has its own geometry; measurement caches never become semantic object state. use crate::{ document::{Frame, MountGeometry, RenderCommand}, text::{self, TextStyle}, ui_bridge::{MAX_DIMENSION, UiKind, UiNode}, }; use anyhow::{Context as _, Result, bail}; use std::collections::{HashMap, HashSet}; // 一次原生整树构建最多展开一万个出现,共享业务对象的不同位置也各计一次。 const MAX_LAYOUT_VISITS: usize = 10_000; #[derive(Debug, Clone, Copy, Default, PartialEq)] /// 上右下左四边的空间占用,用于 margin、padding 和 border 计算。 pub struct Insets { // 顶侧占用,来自上 margin/padding/border 的哪一层由调用方确定。 pub top: f32, // 右侧占用,horizontal 与 left 相加,不含父容器自身位置。 pub right: f32, // 底侧占用,vertical 与 top 相加。 pub bottom: f32, // 左侧占用,安排内容原点时加到父边框盒 x。 pub left: f32, } impl Insets { /// 返回该边距集合在水平方向占据的总空间。 pub fn horizontal(self) -> f32 { self.left + self.right } /// 返回该边距集合在垂直方向占据的总空间。 pub fn vertical(self) -> f32 { self.top + self.bottom } /// 在内容尺寸外加上对应四边占用,得到外层测量尺寸。 fn outer(self, size: Size) -> Size { Size { width: size.width + self.horizontal(), height: size.height + self.vertical(), } } } /// Alignment inside a finite cross-axis content box. Overflow remains start /// aligned so that content cannot become inaccessible beyond the leading edge. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] /// 交叉轴对齐规则;Stretch 需要结合子项是否自动尺寸决定实际拉伸。 pub enum CrossAlign { #[default] // 对齐内容区起点;内容溢出时其他偏移模式也退回起点。 Start, // 按交叉轴非负剩余空间的一半偏移,不改变自然尺寸。 Center, // 按交叉轴全部非负剩余空间偏移。 End, // 偏移为零,尺寸拉伸由 flex_sizes 按 Auto/上下限另行计算。 Stretch, } impl CrossAlign { /// 返回内部枚举或对象对应的稳定源名称。 pub fn name(self) -> &'static str { // 模型字段编码用稳定小写名字,alignSelf 的 auto 不属于此枚举,由 Option 在模型层表示。 match self { Self::Start => "start", Self::Center => "center", Self::End => "end", Self::Stretch => "stretch", } } /// 把外部源码或属性表示转换为内部枚举/结构,并反馈不支持的输入。 pub fn parse(value: &str) -> Result { // 拒绝未知对齐策略;auto 应在调用方转为无自身覆盖,不能解析成某个具体父策略。 match value { "start" => Ok(Self::Start), "center" => Ok(Self::Center), "end" => Ok(Self::End), "stretch" => Ok(Self::Stretch), _ => Err(endor_core::VmError::InvalidArgument( "crossAlign/alignSelf expects start, center, end or stretch (alignSelf also accepts auto)", )), } } /// 根据交叉轴对齐方式计算有限剩余空间中的偏移。 fn offset(self, available: f32, occupied: f32) -> f32 { // 溢出时剩余空间归零,Center/End 都退化为起点对齐,避免把内容前缘移出可达滚动范围。 let spare = (available - occupied).max(0.0); // Stretch 的位置与 Start 相同,实际可拉伸尺寸在分配阶段已经决定。 match self { Self::Start | Self::Stretch => 0.0, Self::Center => spare / 2.0, Self::End => spare, } } } #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] /// 容器主轴的内容对齐或剩余间隔分配方式。 pub enum MainAlign { #[default] // 主轴从内容起点顺序排列。 Start, // 把正剩余空间平分到内容前后,保持既定子项尺寸与固定 gap。 Center, // 将正剩余空间放到首项之前。 End, // 至少两个孩子时把剩余空间均匀加到项间,原固定 gap 仍保留。 SpaceBetween, } impl MainAlign { /// 返回内部枚举或对象对应的稳定源名称。 pub fn name(self) -> &'static str { // 把已验证主轴枚举编码回源属性,字段名与布局分派共享这组名称。 match self { Self::Start => "start", Self::Center => "center", Self::End => "end", Self::SpaceBetween => "space-between", } } /// 把外部源码或属性表示转换为内部枚举/结构,并反馈不支持的输入。 pub fn parse(value: &str) -> Result { // 主轴只支持四个明确策略,不接受交叉轴 stretch 或任意别名。 match value { "start" => Ok(Self::Start), "center" => Ok(Self::Center), "end" => Ok(Self::End), "space-between" => Ok(Self::SpaceBetween), _ => Err(endor_core::VmError::InvalidArgument( "mainAlign expects start, center, end or space-between", )), } } } /// Main-axis constraints interpreted independently by each parent occurrence. #[derive(Debug, Clone, Copy, PartialEq)] /// 主轴伸缩策略与限值,不能用一个宽度常量替代完整分配规则。 pub struct FlexPolicy { // Some 指定主轴首选基准,None 使用首次内容测量;grow/fill 的缺省基准在 flex_sizes 中解释为零。 pub basis: Option, // 主轴弹性分配下限,还要与对应盒模型 min/max 求交。 pub min: f32, // 主轴弹性分配上限,达到后退出剩余空间的再分配。 pub max: f32, // 收缩权重还会乘 basis;默认零表示空间不足时不主动压缩该项。 pub shrink: f32, } impl Default for FlexPolicy { /// 建立类型约定的初始值,供尚未发布或尚未交互的状态使用。 fn default() -> Self { Self { basis: None, min: 0.0, max: MAX_DIMENSION as f32, shrink: 0.0, } } } #[derive(Default, Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)] /// 逻辑宽高值,不隐含窗口像素比例或相机缩放。 pub struct Size { // 逻辑横向长度,测量/分配上下文决定是自然尺寸还是实际边框盒宽。 pub width: f32, // 逻辑纵向长度,不携带 DPI 或相机倍率。 pub height: f32, } #[derive(Debug, Clone, PartialEq)] /// 影响原生测量的缓存形状,避免把颜色等纯绘制变化视为尺寸变化。 enum Shape { // 非默认盒策略把尺寸约束也纳入缓存键,防止同业务对象在不同父分配中误用结果。 Sized(Box, crate::box_model::Sizing, [f32; 4]), // 文本内容、字号等级与有效换行宽都影响测量,颜色不进入键。 Text(String, u32, f32), // 固定自然宽高,亦用于视口、间隔和场景外包范围。 Rect(f32, f32), // 方向、有效 inset、gap 和孩子外尺寸一起决定容器自然尺寸。 Container(bool, Insets, f32, Vec), // 为内容形状套上实际 inset,文字自然测量需加回边框/内边距。 Padded(Box, Insets), } #[derive(Debug, Clone, Hash, PartialEq, Eq)] /// 结构出现身份键,区分同一业务对象在不同父位置的展示。 struct Key { // 父出现 Mount ID;同一业务节点在不同父下要有不同出现身份。 parent: Option, // 父组成边的稳定 slot,不是当前数组下标或屏幕坐标。 slot: u64, // 孩子业务身份文本,替换为不同对象时同一 slot 也不能误复用旧出现。 block: String, } #[derive(Debug, Clone, Default)] /// 布局缓存、稳定出现身份与统计。构建在临时副本上完成,成功后才写回。 pub struct LayoutRuntime { // 按 Block 身份只保留最后一份 Shape/Size;共享 Block 不同约束可交替覆盖,但每次复用前仍比较完整 Shape。 sizes: HashMap, // 出现身份取父 Mount、稳定 slot 和 Block 身份三元组,区别于仅按业务组件分配一个全局 ID。 ids: HashMap, // 已分配 ID 高水位,清出现表或取消重试也不应降低。 next_id: u64, // 每次成功 structure/frame 构建递增,供后续 Mount 与输入归属校验。 pub version: u64, // 本次实际执行 measure_shape 的缓存未命中次数,不是扫描节点总数。 pub measured: usize, // 完整原生构建安排出现次数;结构准备不记为已完成排列。 pub arranged: usize, // 纯原生完整构建记一轮,结构准备记零;源布局驱动另统计其迭代。 pub settle_passes: usize, // 此同步原生构建的工作片统计,源协议可恢复切片由外层驱动维护。 pub slices: usize, // 本窗口逻辑可用大小,未知时使用有限数值的无界哨兵。 pub viewport: Option, // 可选旧 Page 模式,用于生成根轴约束;独立场景视口另有自己的有限盒。 pub page_mode: Option, } /// 迭代遍历中的一个出现,保存父子索引、约束与稳定 ID。 struct Item<'a> { // 借用当前原生不可变视觉节点,构建完成前根树需保持存活。 node: &'a UiNode, // items 中父出现索引,发布时再转换为父 Mount ID。 parent: Option, // items 中有序子出现索引,同一 UiNode Arc 可出现在多个索引。 children: Vec, // 由结构 Key 复用或新分配的本出现 ID。 id: u64, // 此出现独立的 [minW,minH,maxW,maxH],不能按共享业务对象合并。 constraints: [f32; 4], } /// 检查尺寸非负、有限且不超过布局上限。 fn bounded(value: f32) -> Result { // 所有最终测量长度经此边界检查,NaN/负数/超预算都导致本次布局失败。 if !value.is_finite() || !(0.0..=MAX_DIMENSION as f32).contains(&value) { bail!("UI layout exceeds the 1000000 coordinate/size limit"); } Ok(value) } /// 检查允许有正负方向的坐标仍在安全布局范围内。 fn coordinate(value: f32) -> Result { // 位置允许负方向,但有限性和绝对值预算仍约束最终原点及远端。 if !value.is_finite() || value.abs() > MAX_DIMENSION as f32 { bail!("UI coordinate exceeds the 1000000 limit"); } Ok(value) } /// 从页面模式和当前视口生成根节点约束;无视口时使用明确的无界轴哨兵。 pub fn root_constraints(viewport: Option) -> [f32; 4] { // 无视口时以 MAX_DIMENSION 作为无界哨兵;它是有限数值预算,但 Fill 必须另行要求真正有限分配。 let size = viewport.unwrap_or(Size { width: MAX_DIMENSION as f32, height: MAX_DIMENSION as f32, }); [0.0, 0.0, size.width, size.height] } /// 从父级上限扣除内边距;无界哨兵保持无界,不因减法被误认为有限。 fn content_limit(available: f32, inset: f32) -> f32 { // 不能直接从哨兵减 padding,否则略小于 MAX_DIMENSION 会被误认为有限空间,从而错误允许 Fill。 if available >= MAX_DIMENSION as f32 { available } else { (available - inset).max(0.0) } } /// 把父盒子的可用尺寸传递给子项,扣除容器内边距并考虑主轴是否可延伸。 pub fn child_constraints(node: &UiNode, parent: [f32; 4]) -> [f32; 4] { // 先让父自身 boxWidth/Height 与 max 约束进入可用空间,再选择孩子的轴规则。 let parent = node.sizing.available(parent); // Overlay 双轴都受内容盒上限约束;不像 Row/Column 在主轴自然展开。 if let UiKind::Overlay { .. } = node.kind { // Overlay 双轴内容区扣 border+有效 padding,不让孩子共享线性主轴无界规则。 let padding = insets(node); return [ 0.0, 0.0, content_limit(parent[2], padding.horizontal()), content_limit(parent[3], padding.vertical()), ]; } // 二维世界及其观察容器不拿屏幕宽强迫世界内容换行,世界约束使用无界哨兵。 if matches!( node.kind, UiKind::Canvas { .. } | UiKind::Scene2D { .. } | UiKind::CanvasViewport { .. } ) { // 世界 Canvas/Scene2D 与其观察视口不把屏幕尺寸传入世界布局,场景孩子用无界轴约束。 return root_constraints(None); } // 滚动孩子约束基于视口实际分配及允许轴,构造宽高先受父约束夹取。 if let UiKind::Scroll { width, height, .. } = node.kind { return arranged_child_constraints( node, parent, constrained(Size { width, height }, parent), ); } // Row 主轴 x 无界、交叉轴 y 有限;Column/Button/Scroll 的普通方向相反,具体滚动另有上方分支。 match container(node) { Some((true, padding, _)) => [ 0.0, 0.0, MAX_DIMENSION as f32, content_limit(parent[3], padding.vertical()), ], Some((false, padding, _)) => [ 0.0, 0.0, content_limit(parent[2], padding.horizontal()), MAX_DIMENSION as f32, ], None => parent, } } /// 从子项可用范围扣除外边距,并使最小值不超过调整后的最大值。 pub fn margin_constraints(child: &UiNode, mut constraints: [f32; 4]) -> [f32; 4] { // 按横纵轴分别扣两侧 margin,不能用某一轴的总外边距同时改另一轴。 for (axis, margin) in [child.margin.horizontal(), child.margin.vertical()] .into_iter() .enumerate() { // 仅对真正有限轴扣 margin,保持无界哨兵;最小约束同时收窄以维持 min<=max。 if constraints[axis + 2] < MAX_DIMENSION as f32 { constraints[axis + 2] = (constraints[axis + 2] - margin).max(0.0); constraints[axis] = constraints[axis].min(constraints[axis + 2]); } } constraints } /// Main-axis fill first measures against a finite offer, then participates in /// weighted allocation. The unlimited-axis sentinel is never made finite by padding. /// 根据具体子项尺寸策略和所在位置细化约束;世界 UI 根使用面板的有限像素尺寸。 pub fn occurrence_constraints( parent: &UiNode, child: &UiNode, mut base: [f32; 4], offer: Size, child_index: usize, ) -> [f32; 4] { // 世界面板 UI 根必须精确按纹理分辨率布局,最小与最大相同,不让外层视口世界尺寸泄入纹理空间。 if let UiKind::Viewport3D { panels, .. } = &parent.kind { // 面板 child_index 与 panels/children 保持同序,该 UI 根按自己的纹理尺寸精确布局。 let size = panels[child_index]; return [size.width, size.height, size.width, size.height]; } // Fill 初次测量需扣父内容 inset,后面再由 margin_constraints 扣孩子外边距。 let inset = insets(parent); // 两轴独立判断 fill 与当前 offer 是否有限,不能因另一轴有限就允许无界 fill。 for (axis, dimension, available, padding) in [ (0, child.sizing.width, offer.width, inset.horizontal()), (1, child.sizing.height, offer.height, inset.vertical()), ] { // Fill 若从父容器主轴得到无界 base,但父本轮 offer 有限,则用内容可用长度测量,后面再参与权重分配。 if dimension == crate::box_model::Dimension::Fill && base[axis + 2] >= MAX_DIMENSION as f32 && available < MAX_DIMENSION as f32 { base[axis + 2] = (available - padding).max(0.0); base[axis] = base[axis].min(base[axis + 2]); } } margin_constraints(child, base) } /// 生成求解开始时的子项约束,供后续测量和分配迭代使用。 pub fn initial_child_constraints( parent: &UiNode, child: &UiNode, constraints: [f32; 4], child_index: usize, ) -> [f32; 4] { // 父自身盒策略先收窄初次可分配最大值,提供给 occurrence_constraints 补有限 fill offer。 let offer = parent.sizing.available(constraints); occurrence_constraints( parent, child, child_constraints(parent, constraints), Size { width: offer[2], height: offer[3], }, child_index, ) } /// Scroll cross-axis constraints come from this occurrence's actual viewport, /// including a source parent's allocation rather than only constructor size. /// 根据已分配父尺寸重新细化子项约束,使实际安排能够反向影响换行等测量。 pub fn arranged_child_constraints(node: &UiNode, available: [f32; 4], bounds: Size) -> [f32; 4] { // 滚动允许轴无界以容纳内容,禁用轴使用实际已安排的视口尺寸,文字才能按最终交叉轴宽度换行。 if let UiKind::Scroll { axis, .. } = node.kind { // 禁用滚动轴使用此出现已安排 bounds 扣 inset,不能继续按最初构造参数给文字宽度。 let padding = insets(node); return [ 0.0, 0.0, // 允许横向滚动时内容宽使用无界哨兵;禁止横向时用实际内容视口有限宽以驱动换行。 if axis.horizontal() { MAX_DIMENSION as f32 } else { (bounds.width - padding.horizontal()).max(0.0) }, // 纵轴独立判断是否可延伸,禁用轴必须受当前出现 bounds 限制。 if axis.vertical() { MAX_DIMENSION as f32 } else { (bounds.height - padding.vertical()).max(0.0) }, ]; } child_constraints(node, available) } /// 将期望尺寸限制在调用方给出的最小/最大边界内。 pub fn constrained(size: Size, constraints: [f32; 4]) -> Size { Size { width: size.width.clamp(constraints[0], constraints[2]), height: size.height.clamp(constraints[1], constraints[3]), } } /// Effective content inset, including borders, in border-box coordinates. /// 计算布局使用的有效内边距和边框占用,兼容旧的统一 padding。 pub fn insets(node: &UiNode) -> Insets { // 单边 padding 优先,其次兼容容器的统一 padding,最后再加 border;其他叶节点无统一 padding 默认值。 let fallback = match node.kind { UiKind::Row { padding, .. } | UiKind::Column { padding, .. } | UiKind::Button { padding, .. } | UiKind::Scroll { padding, .. } | UiKind::Overlay { padding, .. } => padding, _ => 0.0, }; Insets { top: node.padding_edges[0].unwrap_or(fallback) + node.border.top, right: node.padding_edges[1].unwrap_or(fallback) + node.border.right, bottom: node.padding_edges[2].unwrap_or(fallback) + node.border.bottom, left: node.padding_edges[3].unwrap_or(fallback) + node.border.left, } } /// 提取容器方向与间距等原生布局配置,叶节点不参与容器规则。 fn container(node: &UiNode) -> Option<(bool, Insets, f32)> { // 只识别线性 Row/Column/Button/Scroll,Overlay/世界场景在调用方有独立布局分支。 match &node.kind { UiKind::Row { gap, .. } => Some((true, insets(node), *gap)), UiKind::Scroll { gap, .. } => Some((false, insets(node), *gap)), UiKind::Column { gap, .. } | UiKind::Button { gap, .. } => { Some((false, insets(node), *gap)) } _ => None, } } /// 根据容器方向选择主轴,供 flex 和对齐共享同一判断。 pub fn main_axis(node: &UiNode) -> Option { // row=true 映射横轴 0,纵向映射轴 1;非线性节点返回 None。 container(node).map(|(row, _, _)| usize::from(!row)) } /// 根据形状键计算期望尺寸,包括文字换行、容器子项和盒模型占用。 fn measure_shape(shape: &Shape) -> Result { // 只根据 Shape 计算原生自然/盒策略尺寸,不修改 UiNode、出现身份或任何业务对象。 let size = match shape { // 先测内部内容形状,再由完整盒策略按当前约束求边框盒尺寸。 Shape::Sized(shape, sizing, constraints) => { sizing.resolve(measure_shape(shape)?, *constraints)? } // 内容自然尺寸外加 inset,尺寸结果仍会在函数尾做全局预算检查。 Shape::Padded(shape, padding) => padding.outer(measure_shape(shape)?), Shape::Text(value, size, width) => { // 同一文字样式用于单行自然宽和受限换行高,避免测量两套字体规则。 let style = TextStyle::ui(*size); // 自然文字宽度与受限换行高度分别测量;布局宽度不超过可用宽度,高度反映完整段落行数。 let intrinsic = text::measure(value, style); // 用同一字号在有效宽度下重新排段落高度,不能把自然单行高当作换行高。 let wrapped = text::measure_wrapped(value, style, *width); Size { width: intrinsic.width.min(*width), height: wrapped.height, } } Shape::Rect(width, height) => Size { width: *width, height: *height, }, Shape::Container(row, padding, gap, children) => { // 主轴相加、交叉轴取最大,再加对应 inset 与 n-1 个 gap;saturating_sub 保证零孩子不出现负间距。 let along = children .iter() .map(|s| if *row { s.width } else { s.height }) .sum::() + if *row { padding.horizontal() } else { padding.vertical() } + gap * children.len().saturating_sub(1) as f32; // 交叉轴取最大孩子外尺寸,再加父交叉轴 inset,不把横向孩子高相加。 let cross = children .iter() .map(|s| if *row { s.height } else { s.width }) .fold(0.0_f32, f32::max) + if *row { padding.vertical() } else { padding.horizontal() }; // 将主/交叉轴长度还原为物理 width/height,方向不同不改变计算规则。 if *row { Size { width: along, height: cross, } } else { Size { width: cross, height: along, } } } }; bounded(size.width)?; bounded(size.height)?; Ok(size) } /// 建立影响测量的形状键,将布局属性、子项测量结果和约束组合起来。 fn shape(node: &UiNode, children: Vec, constraints: [f32; 4]) -> Result { // 先应用当前盒策略得到内容可测量上限,文字宽度键必须反映该次出现的有效约束。 let available = node.sizing.available(constraints); // 按 UiKind 提取只影响测量的数据,纯绘制颜色/材质不会制造测量缓存失效。 let shape = match &node.kind { UiKind::Overlay { .. } => { // Overlay 自然尺寸使用孩子含 margin 的最大外框,再加自身有效 inset。 let padding = insets(node); // Overlay 取含各自 margin 的子盒最大宽高,不把重叠项逐个相加。 let size = children.into_iter().zip(node.children()).fold( Size::default(), |size, (child, node)| { // 先把此孩子 margin 加到自身边框盒,避免只看内容尺寸漏算外占用。 let child = node.margin.outer(child); Size { width: size.width.max(child.width), height: size.height.max(child.height), } }, ); Shape::Rect( size.width + padding.horizontal(), size.height + padding.vertical(), ) } // 独立场景视口按自己的有限盒测量,不因远方世界几何或面板内容大小扩展其布局分配。 UiKind::CanvasViewport { width, height, .. } | UiKind::Viewport3D { width, height, .. } => { Shape::Rect(*width, *height) } UiKind::Canvas { positions, .. } | UiKind::Scene2D { positions, .. } => { // 世界内容外包范围从零开始,因此只有负坐标且右下端仍为负时不会扩张正向 extent。 let mut size = Size::default(); // 场景尺寸仅累计孩子右下端最大值;负方向内容不向左上平移整体,导航仍由相机负责。 for (position, child) in positions.iter().zip(children) { size.width = size.width.max(position[0] + child.width); size.height = size.height.max(position[1] + child.height); } Shape::Rect(size.width, size.height) } UiKind::Text { value, size, .. } => { // 文字内容有效宽由本次 available 扣有效 inset,测量后 Padded 再加回。 let padding = insets(node); Shape::Padded( Box::new(Shape::Text( value.clone(), *size, (available[2] - padding.horizontal()).max(0.0), )), padding, ) } UiKind::Rect { width, height, .. } => Shape::Rect(*width, *height), UiKind::Spacer(size) => Shape::Rect(*size, *size), UiKind::Scroll { width, height, .. } => Shape::Rect(*width, *height), _ => { // 剩余种类必须是已知线性容器;未知原生协议明确失败,不猜测为 Column。 let (row, padding, gap) = container(node).context("unknown native visual protocol")?; // 缓存键保存含每个孩子 margin 的外尺寸,margin 改变即使孩子本身尺寸不变也需重测父。 let children = children .into_iter() .zip(node.children()) .map(|(size, child)| child.margin.outer(size)) .collect(); Shape::Container(row, padding, gap, children) } }; // 只有非默认盒尺寸策略才包 Sized 并把约束加入键;文字等自身 Shape 已包含其实际影响测量的参数。 Ok(if node.sizing == crate::box_model::Sizing::default() { shape } else { Shape::Sized(Box::new(shape), node.sizing, constraints) }) } /// 按形状键求原生自然/盒策略尺寸;完整输入 min/max 的最终夹取由原生 Measure 或整树调用方负责。 pub fn measure_native(node: &UiNode, children: Vec, constraints: [f32; 4]) -> Result { measure_shape(&shape(node, children, constraints)?) } /// Private derived scratch space for one occurrence during layout. Contains no /// managed references; source code cannot mutate it through heap attributes. #[derive(Debug, Default)] /// 只读布局调用期间提供给 Mount 查询的派生上下文,不作为业务状态保留。 pub struct Context { // 本轮当前出现的不可变根节点;准备未完成为 None,不可回退旧快照伪装。 pub node: Option>, // 孩子本轮受约束实测尺寸,供原生 measure/arrange 复用而不重复跑源子树。 pub children: Vec, // 本轮首次测量的首选尺寸,不应被上轮 flex 实际分配覆盖,否则反复测量会改变伸缩基准。 pub preferred: Vec, // 排列阶段可用的临时 [x,y,w,h];测量阶段为 None,禁止提前读几何。 pub bounds: Option<[f32; 4]>, } /// 为单个原生节点按已分配几何生成内容命令;容器背景/边框与整树遍历由外层组织。 pub fn paint_native(node: &UiNode, x: f32, y: f32, size: Size) -> Option { // 文字内容盒需要减 inset 并带 wrap_bounds;Rect/Button 背景则画完整 border-box。 // 保留分配 Size,防止 Text 模式中的字号字段 size 遮蔽后误用为像素盒宽高。 let size_for_text = size; // 文字和三维视口绘制内容区,普通矩形/按钮仍使用完整边框盒。 let inset = insets(node); // 每个节点至多生成一条内容/背景命令,边框和孩子由外层 paint_commands/遍历追加。 match &node.kind { // 三维 View 占内容区,扣掉边框/内边距后负面积夹为零,世界矩阵保持不变。 UiKind::Viewport3D { view, .. } => Some(RenderCommand::Scene3D { x: x + inset.left, y: y + inset.top, width: (size.width - inset.horizontal()).max(0.), height: (size.height - inset.vertical()).max(0.), view: view.clone(), }), // 文字原点移到内容起点,wrap_bounds 与本出现实际内容宽高保持一致。 UiKind::Text { value, size, color } => Some(RenderCommand::Text { x: x + inset.left, y: y + inset.top, value: value.clone(), size: *size, color: color.clone(), wrap_bounds: Some([ (size_for_text.width - inset.horizontal()).max(0.0), (size_for_text.height - inset.vertical()).max(0.0), ]), }), // 矩形采用已分配的完整边框盒,自然 width/height 不覆盖父布局结果。 UiKind::Rect { radius, color, .. } => Some(RenderCommand::RoundedRect { x, y, width: size.width, height: size.height, radius: *radius, color: color.clone(), }), // 默认按钮背景覆盖边框盒,固定原生圆角/底色,子文字另外展开。 UiKind::Button { .. } => Some(RenderCommand::RoundedRect { x, y, width: size.width, height: size.height, radius: 7.0, color: "#E3EAF7".into(), }), // 纯容器/场景根没有默认内容命令,不代表其孩子可以跳过绘制。 _ => None, } } /// 从一个节点及其分配几何生成原生绘制命令,包括背景、边框与内容。 pub fn paint_commands(node: &UiNode, x: f32, y: f32, size: Size) -> Vec { // 先收集可选内容命令,再顺序追加四边填充条,保证边框覆盖自身内容边缘。 let mut commands: Vec<_> = paint_native(node, x, y, size).into_iter().collect(); // 边框按四个零圆角条分别追加,宽厚各自钳到盒大小;不使用单一 stroke_width 混淆不等厚四边。 let border = node.border; // 按上、下、左、右四条矩形逐一绘制边框,允许每边厚度不同。 for [x, y, width, height] in [ [x, y, size.width, border.top.min(size.height)], [ x, y + (size.height - border.bottom).max(0.0), size.width, border.bottom.min(size.height), ], [x, y, border.left.min(size.width), size.height], [ x + (size.width - border.right).max(0.0), y, border.right.min(size.width), size.height, ], ] { // 零厚边框无需命令,尺寸已夹到本盒边界,避免负厚度或空面积进入绘制列表。 if width > 0.0 && height > 0.0 { commands.push(RenderCommand::RoundedRect { x, y, width, height, radius: 0.0, color: node.border_color.clone(), }); } } commands } /// Allocate main-axis space with bounded growth and basis-weighted shrinking. /// Preferred sizes come from this occurrence's first measurement in the solve; /// remeasurement must not turn a prior allocation into a new intrinsic basis. /// 判断子项主轴是否采用 fill,从而参与剩余空间分配。 fn main_fill(child: &UiNode, row: bool) -> bool { // 只解释线性父主轴对应的孩子盒策略,交叉轴 fill 不增加主轴增长权重。 (if row { child.sizing.width } else { child.sizing.height }) == crate::box_model::Dimension::Fill } /// 取出主轴 flex/尺寸策略共同约束下的最小和最大分配范围。 fn main_limits(child: &UiNode, row: bool) -> (f32, f32) { // 盒模型提供物理轴的 min/max,下面与 flex_policy 范围求交。 let sizing = child.sizing; // 根据父方向选择孩子的宽或高界限,不能将 flexMin 永远解释为宽度。 let (min, max) = if row { (sizing.min_width, sizing.max_width) } else { (sizing.min_height, sizing.max_height) }; ( child.flex_policy.min.clamp(min, max), child.flex_policy.max.clamp(min, max), ) } /// 按权重分配主轴剩余空间,尊重每项 min/max;达到上限的项退出后重新分配剩余量。 pub fn flex_sizes(node: &UiNode, bounds: Size, children: &[Size], preferred: &[Size]) -> Vec { // Overlay 各孩子独立填内容盒,不共享主轴剩余量;普通线性容器才做下方加权增长/收缩。 if matches!(node.kind, UiKind::Overlay { .. }) { // Overlay 各项独享同一有限内容区,不把空间按兄弟总权重分割。 let inset = insets(node); return children .iter() .zip(node.children()) .map(|(size, child)| { // 复制孩子实测值,只修改符合 fill/stretch 条件的轴,不改共享 UiNode。 let mut size = *size; // Overlay 横轴显式 fill 才占扣 margin 后可用宽,再受盒 min/max 夹取。 if child.sizing.width == crate::box_model::Dimension::Fill { size.width = (bounds.width - inset.horizontal() - child.margin.horizontal()) .max(0.0) .clamp(child.sizing.min_width, child.sizing.max_width); } // Overlay 纵轴 fill 或 auto+stretch 可扩高;显式 fixed 高度不被父对齐覆盖。 if child.sizing.height == crate::box_model::Dimension::Fill || (child.sizing.height == crate::box_model::Dimension::Auto && child.align_self.unwrap_or(node.cross_align) == CrossAlign::Stretch) { size.height = (bounds.height - inset.vertical() - child.margin.vertical()) .max(0.0) .clamp(child.sizing.min_height, child.sizing.max_height); } size }) .collect(); } // 非 Overlay 的非线性节点没有主轴弹性分配,原样返回孩子尺寸。 let Some((row, padding, gap)) = container(node) else { return children.to_vec(); }; // 按视觉树孩子顺序读取策略,和输入 children/preferred 尺寸保持同序。 let nodes = node.children(); // 保留原实测尺寸副本,交叉轴拉伸与主轴重分配依次写到该返回值。 let mut output = children.to_vec(); // 先处理交叉轴 Stretch,仅 Auto 维度可拉伸;明确 Fixed 不因父对齐策略被改写。 // 先按每个孩子 alignSelf 或父 crossAlign 判断交叉轴资格,不依赖主轴是否有增长权重。 for (child, size) in nodes.iter().zip(&mut output) { // 只有有效对齐为 stretch 才进入尺寸改写,center/end 仅在排列时产生偏移。 if child.align_self.unwrap_or(node.cross_align) == CrossAlign::Stretch { // 取得孩子盒轴策略和 min/max,拉伸仍不能超过这些明确界限。 let sizing = child.sizing; // Row 的交叉轴为高度,只拉伸 auto 高,扣父 inset 与孩子上下 margin。 if row && sizing.height == crate::box_model::Dimension::Auto { size.height = (bounds.height - padding.vertical() - child.margin.vertical()) .max(0.0) .clamp(sizing.min_height, sizing.max_height); // 纵向父的交叉轴为宽度,同样只拉伸 auto 宽,不修改 fixed。 } else if !row && sizing.width == crate::box_model::Dimension::Auto { size.width = (bounds.width - padding.horizontal() - child.margin.horizontal()) .max(0.0) .clamp(sizing.min_width, sizing.max_width); } } } // 无主轴伸缩需求时也可能已完成交叉轴拉伸,直接返回这份 output,不丢弃上面调整。 if !nodes.iter().any(|child| child.has_flex()) { return output; } // 可分配主轴空间先扣父 inset、每个孩子两侧 margin 和固定 gap;使用 f64 累计减小权重分配误差。 let space = ((if row { bounds.width } else { bounds.height }) as f64 - if row { padding.horizontal() } else { padding.vertical() } as f64 - nodes .iter() .map(|child| { // 扣除每个孩子在主轴两侧的 margin,不能把横向父的上下 margin 算入可分配宽。 if row { child.margin.horizontal() } else { child.margin.vertical() } }) .sum::() as f64 - gap as f64 * children.len().saturating_sub(1) as f64) .max(0.0); // 显式 basis 优先;grow/fill 默认从零参与分配,其余用 preferred,不拿已经压缩后的 children 充当自然基准。 let bases: Vec = nodes .iter() .zip(preferred) .map(|(child, size)| { child.flex_policy.basis.unwrap_or_else(|| { // 有增长或主轴 fill 且没有显式 basis 时从零参与分配,避免把原自然宽重复计入。 if child.flex > 0.0 || main_fill(child, row) { 0.0 } else if row { size.width } else { size.height } }) as f64 }) .collect(); // 每个原 basis 先夹主轴合法范围,再根据总剩余量决定增长或收缩。 let mut sizes: Vec = nodes .iter() .zip(&bases) .map(|(child, basis)| { // 按孩子当前主轴取 flex/box 交集,这份界限同时用于初始夹取与终点饱和。 let (min, max) = main_limits(child, row); basis.clamp(min as f64, max as f64) }) .collect(); // 初始 basis 先钳 min/max,剩余正数为增长,负数为收缩;非参与项仍占据其合法基准尺寸。 let free = space - sizes.iter().sum::(); // free>0 进入增长,否则是收缩;即使无可分配余量也不会重新发明自然基准。 let growing = free > 0.0; // 增长按 flex,主轴 Fill 且 flex=0 用权重1;收缩按 shrink×原 basis,保持大内容承担较多缩减。 let weights: Vec = nodes .iter() .zip(&bases) .map(|(child, basis)| { // 增长读取 flex(fill 且 flex=0 时给单位权重),收缩读取 shrink×原始 basis。 if growing { // 纯 fill 的默认增长权重为一;显式 flex 则使用其用户权重。 if main_fill(child, row) && child.flex == 0.0 { 1.0 } else { child.flex as f64 } } else { child.flex_policy.shrink as f64 * basis } }) .collect(); // 增长端点是各自 max,收缩端点是各自 min,方便以剩余容量统一分配。 let limits: Vec = nodes .iter() .map(|child| { // 本次取分配终点,策略选择取决于整体 free 的符号,不按单个孩子单独猜测。 let (min, max) = main_limits(child, row); // 选择本轮饱和终点,之后用绝对容量统一处理增长与收缩。 if growing { max as f64 } else { min as f64 } }) .collect(); // 只让有正权重且未到 min/max 的项参与,每轮触顶/到底的项退出,剩余空间继续分给未饱和者。 let mut active: Vec = (0..nodes.len()) .filter(|&i| { weights[i] > 0.0 && if growing { sizes[i] < limits[i] } else { sizes[i] > limits[i] } }) .collect(); // 剩余增长/收缩工作量取绝对值,方向只在更新 sizes 时施加。 let mut remaining = free.abs(); // 只对正权重未饱和项继续分配;饱和项退出后重算剩余权重,避免被夹掉的空间丢失。 while remaining > 0.0 && !active.is_empty() { // 每轮以当前 active 权重总和为分母,不继续把已触顶/触底项计入比例。 let total = active.iter().map(|&i| weights[i]).sum::(); // 累计本轮真实分配量,受单项容量限制后可能小于 remaining。 let mut spent = 0.0; // 记录是否有项达到限制;无饱和时按比例已分完,应停止而不追逐浮点残差。 let mut saturated = false; // 全部项使用同一轮 remaining 比例,不能边更新 remaining 边偏向后项。 for &i in &active { // 本项距离增长/收缩端点的绝对容量,本轮 amount 不得超过它。 let capacity = (limits[i] - sizes[i]).abs(); // 本轮按同一 remaining 比例计算,再限制到该项剩余容量;不能为单项超配后再粗暴钳制丢掉剩余量。 let amount = (remaining * weights[i] / total).min(capacity); sizes[i] += if growing { amount } else { -amount }; spent += amount; saturated |= amount >= capacity; } remaining = (remaining - spent).max(0.0); // Without saturation this round distributed all available space. Do not // spin on floating point residue; a saturated round freezes a child. // 没有项饱和说明比例分配已完成,微小浮点残差不能导致无进展死循环。 if !saturated { break; } // 饱和后筛出仍有容量的项,下一轮只在这些项中重新分配剩余量。 active.retain(|&i| { // 增长保留 sizemin,正权重条件在初始 active 已验证。 if growing { sizes[i] < limits[i] } else { sizes[i] > limits[i] } }); } // 只把最终主轴数值写回 output,交叉轴保留前面的 stretch 结果。 for (size, along) in output.iter_mut().zip(sizes) { // 横向父写 width,纵向父写 height,不能用同一个 Size 字段表示所有主轴。 if row { size.width = along as f32; } else { size.height = along as f32; } } output } /// 根据最终分配尺寸计算子项原点,处理容器方向、对齐、间距与外边距。 pub fn arrange_native( node: &UiNode, x: f32, y: f32, bounds: Size, children: &[Size], ) -> Result> { // 3D 面板根在各自纹理空间均从 (0,0) 开始,世界摆放交由 Panel world 矩阵,不把世界坐标写入普通 UI 布局。 if matches!(node.kind, UiKind::Viewport3D { .. }) { return Ok(node.children().iter().map(|_| (0., 0.)).collect()); } // Overlay 各孩子独立在同一内容盒内对齐,不能按线性游标累加位置。 if let UiKind::Overlay { .. } = node.kind { // 取得含 border 的有效内容 inset,后面的对齐可用范围扣除它。 let padding = insets(node); // Overlay 把 mainAlign 的 center/end 映射为横轴偏移;其他主轴模式在此按 start 处理。 let horizontal = match node.main_align { MainAlign::Center => CrossAlign::Center, MainAlign::End => CrossAlign::End, _ => CrossAlign::Start, }; return children .iter() .zip(node.children()) .map(|(size, child)| { // 对齐比较占用含孩子 margin,最终原点再加左/上 margin。 let outer = child.margin.outer(*size); // 每项独立计算绝对原点,兄弟的大小或数量不改变本项对齐空间。 let point = ( x + padding.left + child.margin.left + horizontal.offset(bounds.width - padding.horizontal(), outer.width), y + padding.top + child.margin.top + child .align_self .unwrap_or(node.cross_align) .offset(bounds.height - padding.vertical(), outer.height), ); coordinate(point.0)?; coordinate(point.1)?; coordinate(point.0 + size.width)?; coordinate(point.1 + size.height)?; Ok(point) }) .collect(); } // 二维视口里的 Scene2D 根先放在内容起点,世界相机投影由后续 viewport 处理。 if matches!(node.kind, UiKind::CanvasViewport { .. }) { // 边框/内边距影响视口内容原点,但不重写场景内部业务 x/y。 let inset = insets(node); return Ok(node .children() .iter() .map(|_| (x + inset.left, y + inset.top)) .collect()); } // 世界布局直接用各孩子业务位置加父原点,不做线性 gap 或主轴对齐。 if let UiKind::Canvas { positions, .. } | UiKind::Scene2D { positions, .. } = &node.kind { return Ok(positions .iter() .map(|point| (x + point[0], y + point[1])) .collect()); } // 其余种类只有线性容器才安排孩子,叶节点返回空位置表。 let Some((row, padding, gap)) = container(node) else { return Ok(vec![]); }; // 按实际分配后的主轴外尺寸与固定 gap 求总占用,margin 不能漏算。 let occupied = children .iter() .zip(node.children()) .map(|(child, node)| { // 根据父方向取宽+左右 margin 或高+上下 margin,交叉尺寸不进入主轴占用。 // 父交叉轴内容尺寸先扣该方向的 inset:Row 用高,纵向父用宽。 if row { child.width + node.margin.horizontal() } else { child.height + node.margin.vertical() } }) .sum::() + gap * children.len().saturating_sub(1) as f32; // 父实际分配的主轴边框盒长度,下一步扣父 inset 与孩子总占用。 let available = if row { bounds.width } else { bounds.height }; // 不足空间时剩余量夹为零,center/end 不产生负偏移让内容前缘不可达。 let spare = (available - if row { padding.horizontal() } else { padding.vertical() } - occupied) .max(0.0); // 主轴 center/end 只分配剩余空白,不再更改 flex 阶段已经求好的尺寸。 let offset = match node.main_align { MainAlign::Center => spare / 2.0, MainAlign::End => spare, _ => 0.0, }; // space-between 只在至少两个孩子间分配额外剩余量,原固定 gap 仍保留;单项保持 start。 let distributed_gap = if node.main_align == MainAlign::SpaceBetween && children.len() > 1 { spare / (children.len() - 1) as f32 } else { 0.0 }; // 线性游标从父起侧 inset 加整体 offset 开始,后续逐项累积外尺寸和间距。 let mut cursor = if row { padding.left } else { padding.top } + offset; // 输出只含孩子原点,顺序对应 children 和 node.children,不携带托管引用。 let mut points = Vec::with_capacity(children.len()); // 逐孩子按实际尺寸、各自 margin 与 alignSelf 计算,避免把共享业务对象的两处位置合并。 for (index, child) in children.iter().enumerate() { // 用同一位置索引取得样式,不能按 block_id 查找唯一孩子而误处理中重复出现。 let child_node = &node.children()[index]; // 本项四边外占用,主轴推进和交叉对齐分别使用对应两边和。 let margin = child_node.margin; // 显式 alignSelf 优先,None 才继承此父出现的 crossAlign。 let align = child_node.align_self.unwrap_or(node.cross_align); // 在父交叉内容尺寸中按孩子外尺寸求非负偏移,不修改主轴游标。 let cross = align.offset( // 孩子交叉外尺寸包含对应两边 margin,与父内容尺寸使用同一物理轴。 if row { bounds.height - padding.vertical() } else { bounds.width - padding.horizontal() }, if row { child.height + margin.vertical() } else { child.width + margin.horizontal() }, ); // 把主轴 cursor 与交叉偏移按方向还原成绝对 x/y,并叠加父与孩子起侧边距。 let point = if row { ( x + cursor + margin.left, y + padding.top + cross + margin.top, ) } else { ( x + padding.left + cross + margin.left, y + cursor + margin.top, ) }; coordinate(point.0)?; coordinate(point.1)?; coordinate(point.0 + child.width)?; coordinate(point.1 + child.height)?; points.push(point); // 主轴游标推进当前盒尺寸、两侧 margin、固定及额外 gap;交叉轴对齐不改变主轴顺序。 cursor += if row { child.width } else { child.height }; cursor += if row { margin.horizontal() } else { margin.vertical() }; cursor += gap + distributed_gap; } Ok(points) } impl LayoutRuntime { /// 从页面模式和当前视口生成根节点约束;无视口时使用明确的无界轴哨兵。 pub fn root_constraints(&self) -> [f32; 4] { // 有旧页面模式时按其有限/延伸轴生成根约束,否则用普通视口最大约束。 self.page_mode.map_or_else( || root_constraints(self.viewport), |mode| mode.constraints(self.viewport), ) } /// 撤销指定业务组件的测量缓存,下一次遇到该节点重新求值。 pub fn invalidate(&mut self, block: &str) { // 只移除指定业务身份最后一份测量键,其他节点缓存和稳定出现 ID 不受此入口影响。 self.sizes.remove(block); } /// 提高新 Mount ID 的分配下限,避免取消/重试后复用已经暴露过的出现身份。 pub fn reserve_identity_floor(&mut self, floor: u64) { // 取消恢复后把临时分配过的 ID 高水位带回,不能重新分配可能已暴露的身份。 self.next_id = self.next_id.max(floor); } /// 返回出现 ID 的当前分配高水位,供桥接层在取消/重试后继续保持唯一性。 pub fn identity_high_watermark(&self) -> u64 { self.next_id } /// 清空结构出现身份表;业务对象和源状态不因此被复制。 pub fn clear_occurrences(&mut self) { // 清出现映射不重置 next_id,高水位继续增加,避免旧事件与新出现数值身份意外重合。 self.ids.clear(); } /// 在临时 LayoutRuntime 中生成完整几何,成功后才替换现有缓存和身份状态。 pub fn frame(&mut self, root: &UiNode) -> Result { // Stage all cache/identity changes; a rejected frame leaves the last // valid occurrence registry intact and never publishes partial geometry. // 缓存、ID 分配和统计都先在副本上变更,build 失败不污染当前运行时。 let mut next = self.clone(); // 完整原生构建成功才提交副本;返回 Frame 仍由桥接添加投影/发布上下文。 let frame = next.build(root, false)?; *self = next; Ok(frame) } /// Allocate stable occurrence identities without measuring or publishing geometry. /// 先建立稳定的出现身份和父子关系,但不把此阶段当成最终测量结果。 pub fn structure(&mut self, root: &UiNode) -> Result { // 结构准备也使用事务式副本,失败不能留下部分新出现映射。 let mut next = self.clone(); // 只分配身份和链接,暂用零几何;后续源布局才提供有效测量与排列结果。 let frame = next.build(root, true)?; *self = next; Ok(frame) } /// 以形状、子项尺寸和约束查缓存;未命中才执行测量并更新本轮统计。 pub fn measure_node( &mut self, node: &UiNode, children: Vec, constraints: [f32; 4], ) -> Result { // 缓存只有完整 Shape 相等才复用,纯颜色变化不在测量 Shape 中,因此无需重做尺寸求解。 let shape = shape(node, children, constraints)?; // 同业务 ID 还必须完整 Shape 相等才命中;不同约束共享对象可能轮流覆盖最后键。 if let Some((_, size)) = self .sizes .get(&node.block_id) .filter(|(old, _)| *old == shape) { return Ok(*size); } self.measured += 1; // 实际未命中才测量,成功后更新 sizes;失败不安装不可用的测量条目。 let size = measure_shape(&shape)?; self.sizes.insert(node.block_id.clone(), (shape, size)); Ok(size) } /// 迭代展开出现树、计算约束和几何、产生绘制命令;结构准备与完整布局共用此驱动。 fn build(&mut self, root: &UiNode, structural: bool) -> Result { // 按本次构建重置计数;structural 只准备身份,不把零几何当一次已完成测量/排列。 self.measured = 0; self.arranged = 0; self.settle_passes = usize::from(!structural); self.slices = usize::from(!structural); // 将树展开为父先子后出现数组,每个位置保存自己的约束与身份。 let mut items = Vec::>::new(); // 用显式栈构建父先子后 items,根采用 slot0/无父;孩子逆序入栈保持原始布局顺序。 let mut work = vec![(root, None::, 0, self.root_constraints())]; // 本轮结构键集合用于检测重复 slot,并在成功后移除过时 ID 映射。 let mut used = HashSet::new(); // 显式 DFS 遍历原生视觉树,共享 Arc 在不同 slot/父下仍会形成独立出现。 while let Some((node, parent, slot, constraints)) = work.pop() { // 按本次展开条目数检查一万上限,超限拒绝整帧而不是截断余下孩子。 if items.len() >= MAX_LAYOUT_VISITS { bail!("expanded UI layout exceeds 10000 nodes"); } // slot 与孩子必须一一对应,否则稳定出现身份不可信,不能用 zip 静默丢掉多余项。 if node.slots.len() != node.children().len() { bail!("invalid child slot table"); } // 稳定身份包含父 Mount ID、组成 slot 和业务 ID;不是仅凭对象身份分配一个 ID。 let key = Key { parent: parent.map(|i| items[i].id), slot, block: node.block_id.clone(), }; // 结构三元组未变则复用 ID,新出现 checked_add 分配;只在成功构建副本后才提交整表。 let id = if let Some(id) = self.ids.get(&key) { *id } else { self.next_id = self .next_id .checked_add(1) .context("Mount identity exhausted")?; self.ids.insert(key.clone(), self.next_id); self.next_id }; // 同一展示出现键不得重复;共享组件在不同父或不同 slot 下使用不同 key,仍允许多次出现。 if !used.insert(key) { bail!("duplicate child slot in one presentation"); } // 保存本出现的数组索引,后面作为孩子 parent 与尺寸/位置数组下标。 let index = items.len(); // 将当前索引追加到父孩子列表,保持实际遍历次序供后序测量与兄弟链接。 if let Some(parent) = parent { items[parent].children.push(index); } items.push(Item { node, parent, children: vec![], id, constraints, }); // 孩子逆序压栈以按原组成顺序出栈,初始约束同时按该出现的父 offer 细化。 for (child_index, (child, slot)) in node.children().iter().zip(&node.slots).enumerate().rev() { work.push(( child.as_ref(), Some(index), *slot, initial_child_constraints(node, child, constraints, child_index), )); } } // 每个出现一个尺寸槽,不按共享业务 ID 合并几何;结构准备保留这些零值占位。 let mut sizes = vec![Size::default(); items.len()]; // 完整路径才反向测量,使每个父求自然尺寸时孩子尺寸已经可用。 if !structural { // 反向 items 自底向上测量,孩子尺寸已经求出;structural=true 完全跳过此步骤,零几何仅用于挂载准备。 for (index, item) in items.iter().enumerate().rev() { sizes[index] = constrained( self.measure_node( item.node, item.children.iter().map(|i| sizes[*i]).collect(), item.constraints, )?, item.constraints, ); } } // 原点按出现数组独立保存,根为零,父排列后写入各子槽。 let mut points = vec![(0.0_f32, 0.0_f32); items.len()]; // 最终发布几何数组容量按出现数预留,但不会在构建失败时替换旧运行状态。 let mut mounts = Vec::with_capacity(items.len()); // 先序展开时按节点内容/边框顺序追加业务命令,后续投影阶段再处理视口和滚动。 let mut commands = Vec::new(); // 默认每个出现无后继,下面只对同一父的相邻孩子建立 next_sibling。 let mut siblings = vec![None; items.len()]; // 先按每个父的孩子顺序建立 next_sibling,最后一个默认 None;这与业务孩子列表身份不同。 // 父先序数组不保证相邻元素就是同级,须显式按每个父的 children 建链接。 for item in &items { // 只把相邻孩子前项连向后项,最后一个保留 None,不串到下个父的子树。 for pair in item.children.windows(2) { siblings[pair[0]] = Some(items[pair[1]].id); } } // 按先序安排,父已先给孩子写入尺寸/原点,当前项可生成对应 Mount 与自身绘制。 for (index, item) in items.iter().enumerate() { self.arranged += usize::from(!structural); // 读取此出现原点,接着检查原点和加宽高后的两个远端都在有符号坐标预算中。 let (x, y) = points[index]; // 取本出现实际尺寸,可能已被父 flex 分配覆盖,不能重新读取业务自然尺寸。 let size = sizes[index]; coordinate(x)?; coordinate(y)?; coordinate(x + size.width)?; coordinate(y + size.height)?; mounts.push(MountGeometry { panel: None, canvas_viewport: matches!(item.node.kind, UiKind::CanvasViewport { .. }), scene3d_viewport: matches!(item.node.kind, UiKind::Viewport3D { .. }), camera_controls: matches!( item.node.kind, UiKind::CanvasViewport { .. } | UiKind::Viewport3D { controls: true, .. } ), viewport_clip: None, view: None, id: item.id, clip: None, scroll: None, block_id: item.node.block_id.clone(), parent: item.parent.map(|i| items[i].id), first_child: item.children.first().map(|i| items[*i].id), next_sibling: siblings[index], x, y, width: size.width, height: size.height, interactive: item.node.interactive, pointer_events: item.node.pointer_events, }); // 只有完整路径生成内容/边框命令,结构准备不得把零几何发布为实际画面。 if !structural { commands.extend(paint_commands(item.node, x, y, size)); } // 结构准备只需要身份/链接,直接跳过孩子尺寸分配和位置安排。 if structural { continue; } // 收集此父孩子当前尺寸作为单次纯原生分配的输入,源协议多轮 remeasure 在外层完成。 let child_sizes: Vec<_> = item.children.iter().map(|child| sizes[*child]).collect(); // 自顶向下用父实际尺寸更新孩子分配后再求原点;更复杂的源协议反复求解由 source_layout 驱动。 let child_sizes = flex_sizes(item.node, size, &child_sizes, &child_sizes); // 按出现索引写回 flex/拉伸实际分配,不改变共享 UiNode 的自然属性。 for (child, allocated) in item.children.iter().zip(&child_sizes) { sizes[*child] = *allocated; } // 把 arrange_native 的同序位置表写入每个子出现槽,之后先序遍历即可消费。 for (child, point) in item.children .iter() .zip(arrange_native(item.node, x, y, size, &child_sizes)?) { points[*child] = point; } } // 成功求解后丢弃已消失出现和不在本树的测量缓存,避免历史页面不断积累派生条目。 self.ids.retain(|key, _| used.contains(key)); // 成功后只保留本树出现过的业务测量键,避免切页后旧派生尺寸持续积累。 let blocks: HashSet<_> = items .iter() .map(|item| item.node.block_id.as_str()) .collect(); self.sizes .retain(|block, _| blocks.contains(block.as_str())); // 每次结构/完整构建成功都推进版本,禁止溢出复用;该版本用于后续 Mount/输入归属校验。 self.version = self .version .checked_add(1) .context("layout version exhausted")?; Ok(Frame { commands, mounts, layout_version: self.version, ..Frame::default() }) } } /// 在临时 LayoutRuntime 中生成完整几何,成功后才替换现有缓存和身份状态。 pub fn frame(root: &UiNode) -> Result { // 一次性调用没有跨帧缓存;需要稳定出现身份的窗口应保留自己的 LayoutRuntime。 LayoutRuntime::default().frame(root) } /// 为指定逻辑视口建立临时布局运行时并求完整帧。 pub fn frame_with_viewport(root: &UiNode, viewport: Size) -> Result { // 为独立原生测量/测试建立有限视口运行时,不创建 VM 或 OS 窗口资源。 LayoutRuntime { viewport: Some(viewport), ..Default::default() } .frame(root) }