1404 lines
66 KiB
Rust
1404 lines
66 KiB
Rust
//! 原生布局求解器:自底向上测量期望尺寸,再自顶向下分配位置和实际尺寸。
|
||
//! 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<Self, endor_core::VmError> {
|
||
// 拒绝未知对齐策略;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<Self, endor_core::VmError> {
|
||
// 主轴只支持四个明确策略,不接受交叉轴 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<f32>,
|
||
// 主轴弹性分配下限,还要与对应盒模型 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<Shape>, crate::box_model::Sizing, [f32; 4]),
|
||
// 文本内容、字号等级与有效换行宽都影响测量,颜色不进入键。
|
||
Text(String, u32, f32),
|
||
// 固定自然宽高,亦用于视口、间隔和场景外包范围。
|
||
Rect(f32, f32),
|
||
// 方向、有效 inset、gap 和孩子外尺寸一起决定容器自然尺寸。
|
||
Container(bool, Insets, f32, Vec<Size>),
|
||
// 为内容形状套上实际 inset,文字自然测量需加回边框/内边距。
|
||
Padded(Box<Shape>, Insets),
|
||
}
|
||
|
||
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
||
/// 结构出现身份键,区分同一业务对象在不同父位置的展示。
|
||
struct Key {
|
||
// 父出现 Mount ID;同一业务节点在不同父下要有不同出现身份。
|
||
parent: Option<u64>,
|
||
// 父组成边的稳定 slot,不是当前数组下标或屏幕坐标。
|
||
slot: u64,
|
||
// 孩子业务身份文本,替换为不同对象时同一 slot 也不能误复用旧出现。
|
||
block: String,
|
||
}
|
||
|
||
#[derive(Debug, Clone, Default)]
|
||
/// 布局缓存、稳定出现身份与统计。构建在临时副本上完成,成功后才写回。
|
||
pub struct LayoutRuntime {
|
||
// 按 Block 身份只保留最后一份 Shape/Size;共享 Block 不同约束可交替覆盖,但每次复用前仍比较完整 Shape。
|
||
sizes: HashMap<String, (Shape, Size)>,
|
||
// 出现身份取父 Mount、稳定 slot 和 Block 身份三元组,区别于仅按业务组件分配一个全局 ID。
|
||
ids: HashMap<Key, u64>,
|
||
// 已分配 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<Size>,
|
||
// 可选旧 Page 模式,用于生成根轴约束;独立场景视口另有自己的有限盒。
|
||
pub page_mode: Option<crate::page::Mode>,
|
||
}
|
||
|
||
/// 迭代遍历中的一个出现,保存父子索引、约束与稳定 ID。
|
||
struct Item<'a> {
|
||
// 借用当前原生不可变视觉节点,构建完成前根树需保持存活。
|
||
node: &'a UiNode,
|
||
// items 中父出现索引,发布时再转换为父 Mount ID。
|
||
parent: Option<usize>,
|
||
// items 中有序子出现索引,同一 UiNode Arc 可出现在多个索引。
|
||
children: Vec<usize>,
|
||
// 由结构 Key 复用或新分配的本出现 ID。
|
||
id: u64,
|
||
// 此出现独立的 [minW,minH,maxW,maxH],不能按共享业务对象合并。
|
||
constraints: [f32; 4],
|
||
}
|
||
|
||
/// 检查尺寸非负、有限且不超过布局上限。
|
||
fn bounded(value: f32) -> Result<f32> {
|
||
// 所有最终测量长度经此边界检查,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<f32> {
|
||
// 位置允许负方向,但有限性和绝对值预算仍约束最终原点及远端。
|
||
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<Size>) -> [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 根必须精确按纹理分辨率布局,最小与最大相同,不让外层视口世界尺寸泄入纹理空间。
|
||
// build 的初始展开、native_visual 的独立 arrange 与 source_layout 收敛都按同一 child_index 解释这些输入,不能按 Block ID 合并共享孩子。
|
||
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<usize> {
|
||
// row=true 映射横轴 0,纵向映射轴 1;非线性节点返回 None。
|
||
container(node).map(|(row, _, _)| usize::from(!row))
|
||
}
|
||
|
||
/// 根据形状键计算期望尺寸,包括文字换行、容器子项和盒模型占用。
|
||
fn measure_shape(shape: &Shape) -> Result<Size> {
|
||
// 只根据 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::<f32>()
|
||
+ 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<Size>, constraints: [f32; 4]) -> Result<Shape> {
|
||
// 先应用当前盒策略得到内容可测量上限,文字宽度键必须反映该次出现的有效约束。
|
||
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<Size>, constraints: [f32; 4]) -> Result<Size> {
|
||
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<std::sync::Arc<UiNode>>,
|
||
// 孩子本轮受约束实测尺寸,供原生 measure/arrange 复用而不重复跑源子树。
|
||
pub children: Vec<Size>,
|
||
// 本轮首次测量的首选尺寸,不应被上轮 flex 实际分配覆盖,否则反复测量会改变伸缩基准。
|
||
pub preferred: Vec<Size>,
|
||
// 排列阶段可用的临时 [x,y,w,h];测量阶段为 None,禁止提前读几何。
|
||
pub bounds: Option<[f32; 4]>,
|
||
}
|
||
|
||
/// 为单个原生节点按已分配几何生成内容命令;容器背景/边框与整树遍历由外层组织。
|
||
pub fn paint_native(node: &UiNode, x: f32, y: f32, size: Size) -> Option<RenderCommand> {
|
||
// 文字内容盒需要减 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<RenderCommand> {
|
||
// 先收集可选内容命令,再顺序追加四边填充条,保证边框覆盖自身内容边缘。
|
||
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<Size> {
|
||
// native_visual::Arrange 与本模块 build 共用;所有输入数组须与 node.children 同序同长度,本函数不替调用方验证平行表。
|
||
// 返回分配尺寸不等于重新测量后的自然尺寸,文字换行等反馈由 source_layout 的后续收敛轮处理。
|
||
// 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::<f32>() as f64
|
||
- gap as f64 * children.len().saturating_sub(1) as f64)
|
||
.max(0.0);
|
||
// 显式 basis 优先;grow/fill 默认从零参与分配,其余用 preferred,不拿已经压缩后的 children 充当自然基准。
|
||
let bases: Vec<f64> = 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<f64> = 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::<f64>();
|
||
// free>0 进入增长,否则是收缩;即使无可分配余量也不会重新发明自然基准。
|
||
let growing = free > 0.0;
|
||
// 增长按 flex,主轴 Fill 且 flex=0 用权重1;收缩按 shrink×原 basis,保持大内容承担较多缩减。
|
||
let weights: Vec<f64> = 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<f64> = 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<usize> = (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::<f64>();
|
||
// 累计本轮真实分配量,受单项容量限制后可能小于 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| {
|
||
// 增长保留 size<max,收缩保留 size>min,正权重条件在初始 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<Vec<(f32, f32)>> {
|
||
// 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,交叉尺寸不进入主轴占用。
|
||
// 这里 child 是实测 Size,node 是对应孩子样式;父自身 inset 在后面的 spare 计算中另扣。
|
||
if row {
|
||
child.width + node.margin.horizontal()
|
||
} else {
|
||
child.height + node.margin.vertical()
|
||
}
|
||
})
|
||
.sum::<f32>()
|
||
+ 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<Frame> {
|
||
// 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 仍由桥接添加投影/发布上下文。
|
||
// build 尚未建立命令 owner/clip/view 与命中索引;正常桥接随后经 scroll::project、viewport::project 重建这些表,不能直接视为可交互发布结果。
|
||
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<Frame> {
|
||
// 结构准备也使用事务式副本,失败不能留下部分新出现映射。
|
||
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<Size>,
|
||
constraints: [f32; 4],
|
||
) -> Result<Size> {
|
||
// 缓存只有完整 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<Frame> {
|
||
// 按本次构建重置计数;structural 只准备身份,不把零几何当一次已完成测量/排列。
|
||
self.measured = 0;
|
||
self.arranged = 0;
|
||
self.settle_passes = usize::from(!structural);
|
||
self.slices = usize::from(!structural);
|
||
// 将树展开为父先子后出现数组,每个位置保存自己的约束与身份。
|
||
let mut items = Vec::<Item<'_>>::new();
|
||
// 用显式栈构建父先子后 items,根采用 slot0/无父;孩子逆序入栈保持原始布局顺序。
|
||
let mut work = vec![(root, None::<usize>, 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 在外层完成。
|
||
// 本路径不会在 flex 改宽后回头重测子树;source_layout 已把需要弹性/拉伸反馈的树排除在小原生直接路径之外。
|
||
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<Frame> {
|
||
// 一次性调用没有跨帧缓存;需要稳定出现身份的窗口应保留自己的 LayoutRuntime。
|
||
LayoutRuntime::default().frame(root)
|
||
}
|
||
|
||
/// 为指定逻辑视口建立临时布局运行时并求完整帧。
|
||
pub fn frame_with_viewport(root: &UiNode, viewport: Size) -> Result<Frame> {
|
||
// 为独立原生测量/测试建立有限视口运行时,不创建 VM 或 OS 窗口资源。
|
||
LayoutRuntime {
|
||
viewport: Some(viewport),
|
||
..Default::default()
|
||
}
|
||
.frame(root)
|
||
}
|