Files
endorphin-tessera-gui/src/render/source_layout.rs
T

1207 lines
62 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 源 measure/arrange 协议的分片布局驱动器。自底向上的测量与自顶向下的安排共享一个只读 VM 调用域。
//! Plan 只保存派生几何和求解过程;Measurement 缓存记录真实读取依赖。已有业务对象在只读阶段不能被修改。
//! 较大树可在节点/指令量子之间挂起。Work 拥有尚未发布的执行状态,必须 resume 或 cancel;完成后才把 Draft 交给桥接层提交。
//!
//! Bottom-up measurement and top-down arrangement share one read-only VM call.
//! The host scratch plan contains derived data only, never a second business heap.
use crate::{
document::Frame,
layout::{self, LayoutRuntime, Size},
mount::Mount,
native_visual::{number, record, value},
object_list,
ui_bridge::{MAX_DIMENSION, UiNode, desktop_type},
};
use anyhow::Result;
use endor_core::{
ContextNativeFunction, NativeCallable, NativeContext, NativeResult, ObjectRef, Value, Vm,
VmError, WeakObjectRef,
};
use std::{
collections::{BTreeSet, HashMap, HashSet},
sync::{Arc, Mutex},
};
// Small native trees keep the allocation-free fast path. Larger trees use the
// same per-occurrence VM driver so even intrinsic work can yield between nodes.
// start 扫描真实协议与 flex 分配后使用此阈值;小原生树可直接求整帧,大树仍需逐出现驱动以便让出。
const NATIVE_FAST_PATH_MAX_NODES: usize = 64;
/// 统一构造本模块的参数错误,让调用方停止处理无效输入。
// 协议解码、缓存跟踪和驱动适配器共用参数错误构造;VM 错误最终由 Work::accept 转为宿主诊断。
fn invalid(message: &'static str) -> VmError {
VmError::InvalidArgument(message)
}
/// 读取当前 desktop 的类型字典身份,供测量缓存过滤宿主内部依赖。
// Collect 结束测量读取跟踪后调用,用来排除宿主类型注册表依赖,而非将其当业务读取缓存。
fn desktop_type_registry(cx: &NativeContext<'_>, desktop: ObjectRef) -> Result<ObjectRef, VmError> {
match cx.store().get_attr(desktop, "types")? {
Value::Object(object) => Ok(object),
_ => Err(invalid("missing type registry")),
}
}
#[derive(Debug, Clone)]
/// 一次源测量的缓存输入、输出和真实读取依赖;不同约束或依赖版本不能复用。
struct Measurement {
// 缓存记录借用的业务身份及方法身份,与子测量、约束和依赖版本共同决定复用资格,不增加托管根。
owner: ObjectRef,
method: ObjectRef,
// 缓存复用条件包含所有子出现的测量尺寸;子尺寸变化即使未写当前 Block 属性也会导致不匹配。
children: Vec<Size>,
// [最小宽,最小高,最大宽,最大高];相同 Block 在不同父约束下不能共用旧测量。
constraints: [f32; 4],
// 已按出现约束钳制的最终测量输出,不保存源返回对象或为它增加拥有根。
size: Size,
// 源调用及返回字段解码的精确读取依赖,Step 逐项检查版本/名称有效性后才复用。
reads: Vec<endor_core::ReadDependency>,
}
/// Borrowed identities and versions only: this cache adds no managed roots.
#[derive(Debug, Default, Clone)]
/// 按 Mount 弱身份保存源测量结果,不为业务对象增加托管根。
pub struct Cache {
// 键为出现弱身份而非共享 Block;移除出现后由 start 清理,源方法/owner 变化由 Step 再检查。
entries: HashMap<WeakObjectRef, Measurement>,
// start 每轮清零的源测量缓存统计,原生 LayoutRuntime 自己维护另一套测量记账。
pub hits: usize,
pub misses: usize,
}
impl Cache {
/// 从测量缓存依赖中移除宿主私有 desktop 记账对象,防止内部同步造成自我失效。
// ui_bridge 合入缓存前传入全部窗口私有 desktop,防止另一窗口宿主记账也成为自我失效依赖。
pub(crate) fn exclude_host_desktops(&mut self, desktops: &HashSet<ObjectRef>) {
// 逐测量项过滤依赖,保留缓存输入与尺寸不变;这里只排除宿主对象,不重新执行 measure。
for entry in self.entries.values_mut() {
entry
.reads
.retain(|read| !desktops.contains(&read.object()));
}
}
/// 移除受影响 owner 的源测量缓存;其他出现继续按其读取版本检查。
// presentation 的显式 invalidate 路径按受影响业务 owner 清理;同 Block 的多个出现缓存会一起移除。
pub fn invalidate(&mut self, owners: &HashSet<ObjectRef>) {
self.entries
.retain(|_, entry| !owners.contains(&entry.owner));
}
/// 清空所有源测量条目和统计,后续源测量再按当前输入重建。
// 小原生树切回直接布局等路径调用;清理源测量条目并重置命中/未命中统计。
pub fn clear(&mut self) {
self.entries.clear();
self.hits = 0;
self.misses = 0;
}
}
#[derive(Debug)]
/// 一个等待源测量返回的收集项,记录本次方法、约束和子项输入。
struct Pending {
// 只读子调用的精确读取跟踪区间 token,由 Step 开始、Collect 在结果解码后结束。
token: usize,
// Step 记下当前 Block 与实际方法,Collect 不再从可能不同的上下文重新猜测缓存身份。
owner: ObjectRef,
method: ObjectRef,
children: Vec<Size>,
constraints: [f32; 4],
// 本次出现弱身份,测量成功后作为 Cache 键;Pending 本身不额外保活 Mount。
key: WeakObjectRef,
}
#[derive(Debug)]
/// 未发布的布局求解工作区,包含出现树、期望尺寸、分配边界、收敛记录和缓存。
struct Plan {
// 使用出现索引保存树和几何,不为每个位置建立第二份业务对象。
nodes: Vec<Arc<UiNode>>,
// 按出现下标保存直接孩子列表,和 nodes/sizes/bounds 一起形成平行数组。
children: Vec<Vec<usize>>,
// 每一轮最新测量值;preferred 则保留自然基准,避免弹性分配反馈后不断漂移。
sizes: Vec<Size>,
// preferred 是测量偏好,bounds 是父级实际分配;换行/flex 会使两者不同。
preferred: Vec<Option<Size>>,
bounds: Vec<Option<[f32; 4]>>,
// 本轮的原生测量运行时与统计副本,完成后才随 Draft 交回桥接层。
runtime: LayoutRuntime,
// 是否实际遇到源布局覆盖;大原生树也走驱动,但不因此发布宿主驱动读取为业务依赖。
custom: bool,
cache: Cache,
// 只为真正执行的源 measure 保存收集状态;原生结果或缓存命中不打开新跟踪区间。
pending: Vec<Option<Pending>>,
constraints: Vec<[f32; 4]>,
// Source-arranged dimensions remain exact for this solve, not business state.
// 每个出现两个轴的显式分配,Some 形成精确 min=max 约束;不是可写业务尺寸属性。
allocations: Vec<[Option<f32>; 2]>,
// 源 arrange 或需要 flex/stretch 的原生安排会记录显式孩子分配,供 Settle 反推精确约束。
custom_arrange: Vec<bool>,
native_arrange: Vec<bool>,
custom_measure: Vec<bool>,
// 源 measure 与默认结果不同的轴作为本次求解的内容上限;新一轮 start 会重新初始化。
measured_limits: Vec<[Option<f32>; 2]>,
// 保存几何状态用于检测收敛和重复状态,防止相互依赖布局无限循环。
previous: Option<Vec<u32>>,
seen: BTreeSet<Vec<u32>>,
}
// Step/Collect/Settle 共享纯原生求解计划;锁用于短时读写,源 VM 方法不在持锁期间执行。
type Shared = Arc<Mutex<Plan>>;
#[derive(Debug, Clone, Copy)]
/// 源视觉调用当前处于测量还是安排阶段,决定调用方法与结果解码。
enum Phase {
Measure,
Arrange,
}
impl Phase {
/// 把当前布局阶段映射为 measure 或 arrange 方法名称。
fn method(self) -> &'static str {
match self {
Self::Measure => "measure",
Self::Arrange => "arrange",
}
}
}
#[derive(Debug)]
/// 为某次出现准备一个 measure/arrange 调用,选择原生快速结果或真正的源覆盖。
struct Step {
// 当前步骤共享计划;index/phase 指定一次出现的一次测量或安排。
plan: Shared,
index: usize,
phase: Phase,
// Advance 为当前出现升级并显式捕获的强身份,仅当前辅助调用需要保活它。
mount: ObjectRef,
desktop: ObjectRef,
}
impl NativeCallable for Step {
/// 单个布局步骤所需 Mount、阶段和计划已保存,实际调用无参。
fn arity(&self) -> Option<endor_core::CallArity> {
Some(endor_core::CallArity::exact(0))
}
/// 解析当前实际 measure/arrange 实现,决定原生快速路径或源调用,准备本节点输入及依赖收集,避免跳过源覆盖。
fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result<NativeResult, VmError> {
// 先克隆 Mount 描述以跨后续 VM 分配使用;它带当前 Block 和临时布局上下文。
let mount = cx.store().payload::<Mount>(self.mount, "Mount")?.clone();
// 沿当前业务 Block 的真实类型查方法,不能只按节点种类判断有没有源覆盖。
let definition = cx
.store()
.type_of(mount.block)?
.ok_or(invalid("untyped layout target"))?;
// phase.method 决定 measure/arrange,缺少协议方法视为错误而不是静默回退。
let method = cx
.store()
.lookup_method(definition, self.phase.method())?
.ok_or(invalid("missing layout method"))?;
// 判断真实宿主实现而非方法名;源覆盖即使调用基类也不能整体绕过其用户逻辑。
let intrinsic = crate::native_visual::is_intrinsic(
cx.store(),
method,
self.phase.method(),
self.desktop,
)?;
// 在短锁内取得当前出现输入并更新计划记账,退出后才构造托管参数及真正源调用。
let (node, children, preferred, bounds, native_size) = {
let mut plan = self
.plan
.lock()
.map_err(|_| invalid("layout plan poisoned"))?;
// Arc 克隆只共享不可变视觉节点,children 则复制本轮已完成的直接子测量。
let node = plan.nodes[self.index].clone();
let children = plan.children[self.index]
.iter()
.map(|child| plan.sizes[*child])
.collect::<Vec<_>>();
// preferred 优先取本轮最初测量值,不能将上一轮 flex 分配当作新的自然基准,造成累计漂移。
let preferred = plan.children[self.index]
.iter()
.map(|child| plan.preferred[*child].unwrap_or(plan.sizes[*child]))
.collect::<Vec<_>>();
// measure 不暴露旧安排边界;arrange 必须使用父级已分配的本轮 bounds。
let bounds = if matches!(self.phase, Phase::Arrange) {
plan.bounds[self.index]
} else {
None
};
// 每次 measure 重新记录实际是否源覆盖,Collect 据此决定是否比较默认测量以生成额外内容限制。
if matches!(self.phase, Phase::Measure) {
plan.custom_measure[self.index] = !intrinsic;
}
// 原生 measure 直接计算避免源 VM 调用;源 measure 尝试按 Mount、owner、方法、子尺寸、约束和读取版本复用。
let native_size = if matches!(self.phase, Phase::Measure) && intrinsic {
// 原生快路径也使用当前出现约束,不直接返回脱离父可用空间的自然尺寸。
let constraints = plan.constraints[self.index];
Some(
plan.runtime
.measure_node(&node, children.clone(), constraints)
.map_err(|error| {
error.downcast_ref::<VmError>().cloned().unwrap_or_else(|| {
invalid("native measurement exceeds layout limits")
})
})?,
)
} else {
// 遇到任一源协议就标记整轮 custom,finish 才会保留过滤后的业务布局读取。
if !intrinsic {
plan.custom = true;
}
if matches!(self.phase, Phase::Measure) {
// 测量缓存按本次 Mount 弱身份查找,仍需同时验证 owner、方法、孩子输入、约束及依赖。
let key = cx.store().downgrade(self.mount)?;
let constraints = plan.constraints[self.index];
// 只有所有条件同时满足才取旧尺寸;值存在但依赖过期与完全无缓存都走未命中分支。
let cached =
plan.cache
.entries
.get(&key)
.filter(|entry| {
entry.owner == mount.block
&& entry.method == method
&& entry.children == children
&& entry.constraints == constraints
&& entry.reads.iter().all(|dependency| {
cx.store().read_dependency_valid(dependency)
})
})
.map(|entry| entry.size);
// 命中无需再打开读取跟踪;未命中保存 Pending 元信息,等待真正源调用结果返回才建立缓存。
if let Some(size) = cached {
plan.cache.hits += 1;
Some(size)
} else {
plan.cache.misses += 1;
plan.runtime.measured += 1;
// token 先占位为 0;输入 Constraints 初始化完成后才开启精确跟踪并写入真实 token。
plan.pending[self.index] = Some(Pending {
token: 0,
owner: mount.block,
method,
children: children.clone(),
constraints,
key,
});
None
}
} else {
None
}
};
// 每次安排都计数,并记录是否原生及是否需要显式分配,后面的收敛阶段依赖这两个标记。
if matches!(self.phase, Phase::Arrange) {
plan.runtime.arranged += 1;
plan.native_arrange[self.index] = intrinsic;
// 原生 arrange 若需要分配 flex/stretch 也视作需显式记录子分配,参与后面的约束收敛。
plan.custom_arrange[self.index] = !intrinsic || node.allocates_children();
}
(node, children, preferred, bounds, native_size)
};
// 先安装本轮节点、子测量和临时 bounds,让源代码调用基类或 Mount.bounds 时观察当前阶段数据。
let context = mount.layout.ok_or(invalid("missing pre-layout context"))?;
{
let mut context = context
.lock()
.map_err(|_| invalid("layout context poisoned"))?;
context.node = Some(node);
context.children = children;
context.preferred = preferred;
context.bounds = bounds;
}
// 原生结果或缓存命中都包装为同一名义 MeasureResult,后续 Collect 无需区分来源。
if let Some(size) = native_size {
return Ok(NativeResult::Return(Value::Object(record(
cx,
self.desktop,
"MeasureResult",
vec![("width", value(size.width)), ("height", value(size.height))],
)?)));
}
// 构造本桌面名义输入对象:measure 接收 Constraints,arrange 接收已分配 Bounds。
let input = match self.phase {
// 从 Plan 取当前约束后立刻释放短锁,record 的对象分配发生在锁外。
Phase::Measure => {
let constraints = self
.plan
.lock()
.map_err(|_| invalid("layout plan poisoned"))?
.constraints[self.index];
record(
cx,
self.desktop,
"Constraints",
vec![
("minWidth", value(constraints[0])),
("minHeight", value(constraints[1])),
("maxWidth", value(constraints[2])),
("maxHeight", value(constraints[3])),
],
)?
}
// 父级没有分配当前边界时不能调用 arrange,否则孩子无法得到可靠的绝对布局坐标。
Phase::Arrange => {
let [x, y, width, height] =
bounds.ok_or(invalid("arrangement has no allocated bounds"))?;
record(
cx,
self.desktop,
"Bounds",
vec![
("x", value(x)),
("y", value(y)),
("width", value(width)),
("height", value(height)),
],
)?
}
};
if matches!(self.phase, Phase::Measure) {
// 输入记录初始化完成后再开始子调用跟踪;Collect 解码返回值也在区间内,覆盖复用旧结果对象的字段读取。
let token = cx.store().begin_read_tracking()?;
self.plan
.lock()
.map_err(|_| invalid("layout plan poisoned"))?
.pending[self.index]
.as_mut()
.ok_or(invalid("missing measurement tracking state"))?
.token = token;
}
// 交回 VM 调用 method(block, mount, input),结果由 Advance 安排的 Collect 统一收集。
Ok(NativeResult::Invoke {
function: method,
arguments: vec![
Value::Object(mount.block),
Value::Object(self.mount),
Value::Object(input),
],
})
}
}
#[derive(Debug)]
/// 接收并检查源协议返回结果,将其合入 Plan,而不是直接写成发布帧。
struct Collect {
plan: Shared,
index: usize,
phase: Phase,
desktop: ObjectRef,
}
impl NativeCallable for Collect {
/// 接收一次 measure/arrange 返回的托管结果,按当前 phase 解码。
fn arity(&self) -> Option<endor_core::CallArity> {
Some(endor_core::CallArity::exact(1))
}
/// 结束当前测量读跟踪并解码结果,验证尺寸或完整子槽安排,再更新计划与可复用测量依赖。
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
// 取走本节点待收集状态,确保每个跟踪区间只结束一次;arrange 不建立测量缓存项。
let pending = if matches!(self.phase, Phase::Measure) {
self.plan
.lock()
.map_err(|_| invalid("layout plan poisoned"))?
.pending[self.index]
.take()
} else {
None
};
// 原生快路径、缓存包装和源覆盖都必须返回一个托管结果,不能用裸数字或 Nil 代替协议。
let [Value::Object(result)] = args else {
return Err(invalid("layout must return a managed result"));
};
// 阶段选择本桌面名义结果类型;后面的字段解码不能替代 is_instance 检查。
let expected = match self.phase {
Phase::Measure => "MeasureResult",
Phase::Arrange => "Arrangement",
};
// 先验证 MeasureResult/Arrangement 名义类型,再解码字段,拒绝只有同名字段的普通对象。
if !cx
.store()
.is_instance(*result, desktop_type(cx.store(), expected, self.desktop)?)?
{
return Err(invalid("layout returned the wrong result type"));
}
// 结果已经回到宿主,持锁更新当前计划;这里不再调用源 measure/arrange。
let mut plan = self
.plan
.lock()
.map_err(|_| invalid("layout plan poisoned"))?;
match self.phase {
Phase::Measure => {
// 源返回宽高先检查非负有限,再钳制到本出现约束,不允许结果绕过父级分配。
let size = layout::constrained(
Size {
width: number(cx, *result, "width", false)?,
height: number(cx, *result, "height", false)?,
},
plan.constraints[self.index],
);
// Native shrink-to-content containers keep their existing input
// policy. Only a source measurement that differs from the native
// result supplies an additional content constraint.
// 源容器测量若不同于默认原生结果,将相应轴记成额外内容上限;后续碰巧相等也不撤销本轮已选限制。
if plan.custom_measure[self.index] && !plan.children[self.index].is_empty() {
// 用本轮孩子尺寸重新计算默认测量作比较,只在源容器有孩子时引入额外内容上限。
let children = plan.children[self.index]
.iter()
.map(|child| plan.sizes[*child])
.collect();
// 原生测量失败时转换成 None,下面两轴都会保守保留源尺寸限制,而不是忽略源返回值。
let native = layout::measure_native(
&plan.nodes[self.index],
children,
plan.constraints[self.index],
)
.ok()
.map(|native| layout::constrained(native, plan.constraints[self.index]));
// Equality after remeasurement does not retract a source
// limit selected earlier in this solve. New renders start fresh.
// 只设置发生差异的轴;此前已记录的限制在本次求解中保持,不因后续尺寸巧合相等而撤销。
if !native.is_some_and(|native| native.width == size.width) {
plan.measured_limits[self.index][0] = Some(size.width);
}
if !native.is_some_and(|native| native.height == size.height) {
plan.measured_limits[self.index][1] = Some(size.height);
}
}
// Decoding is part of the subcall dependency region: a source
// method may return an existing result whose fields can change.
// 原生或缓存命中没有 Pending,不结束不存在的跟踪区间;源测量在解码宽高之后结束,覆盖结果字段读取。
let tracked = pending
.map(|pending| {
cx.store()
.end_read_tracking_precise(pending.token)
.map(|reads| (pending, reads))
})
.transpose()?;
// 只有真正源测量才尝试写缓存;精确访问记录还需判断是否读取了不稳定的 Mount 内容。
if let Some((pending, reads)) = tracked {
// 若观察了 Mount 内容/几何等不稳定宿主状态,无法证明跨轮可复用,保守地删除该测量缓存。
let mut volatile = false;
let registry = desktop_type_registry(cx, self.desktop)?;
// accesses 提供内容读取细节,dependencies 则用于后面保留可校验依赖,两者用途不同。
for read in &reads.accesses {
let object = read.object;
// 当前桌面和类型注册表属于宿主内部检查,跳过这类访问而不把它们认成易变业务输入。
if object == self.desktop || object == registry {
continue;
}
// A Mount's layoutVersion/geometry may be observed by
// source. Object-granular tracking cannot prove those
// accesses stable, so conservatively bypass its cache.
if cx.store().object(object)?.payload().as_any().is::<Mount>() {
volatile |= read.content_read;
continue;
}
}
// 观测到 Mount 内容读取就移除旧项,本次仍使用已得到的尺寸,只是不允许下次复用。
if volatile {
plan.cache.entries.remove(&pending.key);
} else {
plan.cache.entries.insert(
pending.key,
Measurement {
owner: pending.owner,
method: pending.method,
children: pending.children,
constraints: pending.constraints,
size,
reads: reads
.dependencies
.into_iter()
.filter(|dependency| {
let object = dependency.object();
// 缓存只保留仍存在的业务依赖,去除 desktop、注册表与 Mount;避免宿主提交几何自己反过来使缓存失效。
object != self.desktop
&& object != registry
&& cx.store().object(object).is_ok_and(|value| {
!value.payload().as_any().is::<Mount>()
})
})
.collect(),
},
);
}
}
// 仅首次填写 preferred;sizes 每轮更新,根测量直接给本轮根 bounds,安排才能从根向下展开。
plan.preferred[self.index].get_or_insert(size);
plan.sizes[self.index] = size;
// 根没有父安排可提供 bounds,直接以本轮测量从 (0,0) 建立根边界,随后正序 arrange 展开孩子。
if self.index == 0 {
plan.bounds[0] = Some([0.0, 0.0, size.width, size.height]);
}
}
Phase::Arrange => {
// Arrangement.children 必须指向受控对象列表;先取全部项,再按稳定 slot 分配到计划孩子。
let Value::Object(list) = cx.store().get_attr(*result, "children")? else {
return Err(invalid("Arrangement.children must be an ObjectList"));
};
let items = object_list::items(cx.store(), list)?;
let children = plan.children[self.index].clone();
// 安排结果必须覆盖全部孩子,不能省略零尺寸项;结合下面 slot 唯一检查保证逐一对应。
if items.len() != children.len() {
return Err(invalid(
"Arrangement must assign every child slot exactly once",
));
}
// 按稳定 slot 找实际孩子下标,不要求源返回列表与孩子原顺序一致;外来或重复 slot 都拒绝。
let slots: HashMap<_, _> = plan.nodes[self.index]
.slots
.iter()
.copied()
.enumerate()
.map(|(index, slot)| (slot, index))
.collect();
// 记录已安排 slot 防止重复,项数相等加逐项唯一性共同保证每个孩子恰好分配一次。
let mut assigned = BTreeSet::new();
let expected = desktop_type(cx.store(), "Bounds", self.desktop)?;
// 源返回列表可以重新排序,实际孩子用 slot 查表确定,不按列表位置直接对应。
for item in items {
// 每项需是本桌面 Bounds,之后 slot 和几何字段仍逐项校验。
if !cx.store().is_instance(*item, expected)? {
return Err(invalid("Arrangement child must be Bounds"));
}
let Value::Number(slot) = cx.store().get_attr(*item, "slot")? else {
return Err(invalid("Arrangement child requires a slot"));
};
// slot 先要求可转为 i64 整数,再拒绝负值转为 u64;超出 i64 范围的值也不能作为此处身份。
let slot = slot
.as_i64()
.and_then(|value| u64::try_from(value).ok())
.ok_or(invalid("invalid arrangement slot"))?;
// 只接受当前父节点 slots 表中的身份,表外身份不能混入安排。
let position = slots
.get(&slot)
.copied()
.ok_or(invalid("Arrangement references a foreign child slot"))?;
// 重复项不能覆盖同一个孩子后遗漏另一个孩子,即使总体列表长度正确也要失败。
if !assigned.insert(slot) {
return Err(invalid("duplicate arrangement slot"));
}
// x/y 可负表示相对布局延伸,宽高必须非负且有限;下面还检查右下组合边界。
let bounds = [
number(cx, *item, "x", true)?,
number(cx, *item, "y", true)?,
number(cx, *item, "width", false)?,
number(cx, *item, "height", false)?,
];
// 单项数值合法仍可能在相加后超出几何预算,不能只检查四个字段各自范围。
if bounds[0] + bounds[2] > MAX_DIMENSION as f32
|| bounds[1] + bounds[3] > MAX_DIMENSION as f32
{
return Err(invalid("arrangement bounds exceed limits"));
}
// 将 slot 表内的位置转换为 Plan 的全局出现下标,用于写 bounds 与轴分配。
let child = children[position];
// 源安排或原生弹性分配的差异保留为显式轴分配,下一轮测量使用精确尺寸,确保文字按实际宽度重排。
if plan.custom_arrange[self.index] {
// 原生主轴 flex 分配即使恰好等于当前测量也显式保存,避免下一次父约束传播撤销已分配空间。
if plan.native_arrange[self.index]
&& let Some(main) = layout::main_axis(&plan.nodes[self.index])
{
// Keep an allocation explicit even if this pass's
// natural size happens to match it. It must survive
// the next parent-constraint propagation pass.
plan.allocations[child][main] = Some(bounds[main + 2]);
}
// 源或弹性安排改变宽/高时分别设置精确轴分配,未变化且未显式要求的轴仍可按自然测量工作。
if bounds[2] != plan.sizes[child].width {
plan.allocations[child][0] = Some(bounds[2]);
}
if bounds[3] != plan.sizes[child].height {
plan.allocations[child][1] = Some(bounds[3]);
}
}
// 保存本轮孩子绝对布局边界;正序 Arrange 随后轮到该孩子时才能取得自己的输入。
plan.bounds[child] = Some(bounds);
}
}
}
Ok(NativeResult::Return(Value::Nil))
}
}
#[derive(Debug)]
/// 检查本轮布局是否已收敛,必要时安排下一次测量/分配迭代。
struct Settle {
plan: Shared,
mounts: Arc<[WeakObjectRef]>,
}
impl NativeCallable for Settle {
/// 一轮测量/排列完成后无参进入收敛检查。
fn arity(&self) -> Option<endor_core::CallArity> {
Some(endor_core::CallArity::exact(0))
}
/// 由新 bounds 推导下一轮约束与自然尺寸,比较完整布局状态;稳定则结束,循环状态报错,否则继续同帧求解。
fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result<NativeResult, VmError> {
// 收敛计算持 Plan 短锁,后面的临时 Mount 上下文清理放在锁外执行。
let repeat = {
let mut plan = self
.plan
.lock()
.map_err(|_| invalid("layout plan poisoned"))?;
plan.runtime.settle_passes += 1;
// 安排完成后按最终 bounds 从父到子推导下一轮约束,测量限制和源分配可反向影响换行高度。
let mut next = plan.constraints.clone();
// 按父先子后传播下一轮约束,next[parent] 先确定后才推导直接孩子。
for index in 0..plan.nodes.len() {
let children = plan.children[index].clone();
let [_, _, width, height] =
plan.bounds[index].ok_or(invalid("unarranged occurrence"))?;
// 先继承本节点下一轮约束,再用源测量限制和实际已分配宽高调整可供孩子使用的上界。
let mut available = next[index];
// 源测量选择的内容上限优先加入 available,后面实际分配差异也可能继续覆盖该轴。
if let Some(width) = plan.measured_limits[index][0] {
available[2] = width;
}
if let Some(height) = plan.measured_limits[index][1] {
available[3] = height;
}
// 父级实际分配与测量不同,孩子的可用宽度应跟随分配,否则换行仍按旧宽度计算。
if width != plan.sizes[index].width {
available[2] = width;
}
if height != plan.sizes[index].height {
available[3] = height;
}
// 根据父盒模型与实际宽高计算孩子公共约束基础,随后再考虑各个出现的独立尺寸规则。
let base = layout::arranged_child_constraints(
&plan.nodes[index],
available,
Size { width, height },
);
// child_index 是父内顺序,child 是全局出现下标,两者不能互换用于平行数组。
for (child_index, child) in children.into_iter().enumerate() {
// 保留未施加精确轴分配的自然约束;交叉轴改变时,撤销旧原生主轴分配需要用它恢复。
let natural = layout::occurrence_constraints(
&plan.nodes[index],
&plan.nodes[child],
base,
Size { width, height },
child_index,
);
let mut constraints = natural;
// 显式轴分配同时设置最小与最大,区别于仅给孩子一个可自由缩小的最大可用宽度。
if let Some(width) = plan.allocations[child][0] {
constraints[0] = width;
constraints[2] = width;
}
if let Some(height) = plan.allocations[child][1] {
constraints[1] = height;
constraints[3] = height;
}
// 线性布局才有主/交叉轴;交叉轴范围变化可触发文字重排,进而改变主轴自然尺寸。
if let Some(main) = layout::main_axis(&plan.nodes[index]) {
let cross = 1 - main;
// 交叉轴变化可能改变换行及主轴自然高度,清 preferred 让其重新测量;原生派生主轴分配也需暂时撤销。
if constraints[cross] != plan.constraints[child][cross]
|| constraints[cross + 2] != plan.constraints[child][cross + 2]
{
// Wrapping or other cross-dependent measurement
// invalidates this occurrence's natural main size.
plan.preferred[child] = None;
// 只撤销原生派生主轴分配,源 arrange 的明确分配仍是权威约束,不能被自动回退覆盖。
if plan.native_arrange[index] {
// Derived flex sizes must not constrain their
// own replacement measurement. Source arrange
// allocations remain authoritative.
plan.allocations[child][main] = None;
constraints[main] = natural[main];
constraints[main + 2] = natural[main + 2];
}
}
}
next[child] = constraints;
}
}
// 状态向量用于相邻稳定比较及历史重复检测;capacity 只是预分配提示,不限制最终编码长度。
let mut state = Vec::with_capacity(plan.nodes.len() * 14);
// 把约束、实测、bounds、分配和首选值编码成几何状态,统一 +0/-0,避免仅符号位差异造成假不收敛。
let bits = |value: f32| if value == 0.0 { 0 } else { value.to_bits() };
// 编码每次出现的完整参与状态,不能只比较根宽高,否则内部布局仍变化时可能提前停止。
for (index, constraints) in next.iter().enumerate() {
state.extend(constraints.iter().copied().map(bits));
state.extend([
bits(plan.sizes[index].width),
bits(plan.sizes[index].height),
]);
state.extend(
plan.bounds[index]
.ok_or(invalid("unarranged occurrence"))?
.into_iter()
.map(bits),
);
// Option 先编码存在标记再编码数值,使 None 和显式 Some(0) 在收敛判断中保持不同。
for dimension in plan.allocations[index] {
state.push(u32::from(dimension.is_some()));
state.push(dimension.map_or(0, bits));
}
state.push(u32::from(plan.preferred[index].is_some()));
// 首选尺寸同样带存在标记;缺失时写两个零占位以维持每项固定字段布局。
if let Some(size) = plan.preferred[index] {
state.extend([bits(size.width), bits(size.height)]);
} else {
state.extend([0, 0]);
}
}
// 相邻两轮状态和约束均相同才停止;非相邻重复则是振荡环,不能把循环误当作稳定布局。
if plan.previous.as_ref() == Some(&state) && plan.constraints == next {
false
} else {
// 与紧邻上轮不同却命中历史状态,说明出现振荡环,返回错误而不是无限重试。
if !plan.seen.insert(state.clone()) {
return Err(invalid("layout constraints did not converge (cycle)"));
}
plan.previous = Some(state);
plan.constraints = next;
// 需要重算时清全部 bounds;下一轮测量不能访问上轮安排产生的陈旧几何。
plan.bounds.fill(None);
true
}
};
// 只有需要新一轮才清理 VM 可观察的临时 Bounds;稳定结束时保留最终安排供后续提交。
if repeat {
// No stale geometry from a prior pass is observable during measurement.
// 逐项升级当前布局 Mount,清除上下文旧 bounds,防止下一轮 measure 观察上一轮安排几何。
for weak in self.mounts.iter() {
let mount = cx.upgrade(*weak)?.ok_or(invalid("layout Mount expired"))?;
if let Some(context) = &cx.store().payload::<Mount>(mount, "Mount")?.layout {
context
.lock()
.map_err(|_| invalid("layout context poisoned"))?
.bounds = None;
}
}
}
Ok(NativeResult::Return(Value::Bool(repeat)))
}
}
#[derive(Debug)]
/// 准备当前求解步骤所需的约束和 Mount 临时上下文。
struct Prepare {
plan: Shared,
mounts: Vec<WeakObjectRef>,
desktop: ObjectRef,
}
impl NativeCallable for Prepare {
/// 布局驱动的零参入口,Mount 列表/desktop/计划已由调用方捕获。
fn arity(&self) -> Option<endor_core::CallArity> {
Some(endor_core::CallArity::exact(0))
}
/// 建立按出现位置推进的 Advance 和固定字节码循环,保持同一 VM 预算,避免首次让出前创建整树临时调用对象。
fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result<NativeResult, VmError> {
// Prepare 只建立共享游标适配器和固定循环;不会在首次让出前一次创建全树 Step/Collect。
let advance = cx.allocate(ContextNativeFunction::new(
"next layout occurrence",
Advance {
plan: self.plan.clone(),
mounts: self.mounts.clone().into(),
desktop: self.desktop,
next: Mutex::new(0),
},
vec![self.desktop],
))?;
let function = crate::vm_sequence::repeat_call(cx, advance, "source layout", self.desktop)?;
Ok(NativeResult::Invoke {
function,
arguments: vec![],
})
}
}
#[derive(Debug)]
/// 在同一驱动帧中安排下一项布局工作,使大树能够分片让出。
struct Advance {
plan: Shared,
mounts: Arc<[WeakObjectRef]>,
desktop: ObjectRef,
// 每轮 [0,N) 为逆序测量,[N,2N) 为正序安排,2N 专门进入 Settle。
next: Mutex<usize>,
}
impl NativeCallable for Advance {
/// 由共享布局循环无参调用,游标表示本轮测量/排列的位置。
fn arity(&self) -> Option<endor_core::CallArity> {
Some(endor_core::CallArity::exact(0))
}
/// 按游标安排自底向上 measure 与自顶向下 arrange,短锁结束后才创建/执行当前节点调用,末尾进入收敛检查。
fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result<NativeResult, VmError> {
// mounts 与 Plan 出现数组同序等长,start 已检查数量,游标借这个长度选择阶段和下标。
let count = self.mounts.len();
// 游标取值/递增只在短锁内完成,不能持锁执行源协议,否则源调用基类会再次访问同一 Plan。
let position = {
let mut next = self
.next
.lock()
.map_err(|_| invalid("layout cursor poisoned"))?;
let position = *next;
// 只在节点步骤范围内递增;末尾 Settle 自己决定重置为 0 或让外层循环停止。
if position < count * 2 {
*next += 1;
}
position
};
// 每轮恰有 N 个逆序 measure 和 N 个正序 arrange,之后调用 Settle;需要重算时游标归零。
if position == count * 2 {
// 所有节点完成 measure/arrange 才调用收敛检查;它返回是否需要下一轮,不再增加独立 VM 总预算。
let settled = Settle {
plan: self.plan.clone(),
mounts: self.mounts.clone(),
}
.invoke(cx, &[])?;
let NativeResult::Return(Value::Bool(repeat)) = settled else {
return Err(invalid("invalid settling result"));
};
// 需要继续时重置同一个游标,后续仍回到原 repeat_call 帧执行整轮;false 则结束布局。
if repeat {
*self
.next
.lock()
.map_err(|_| invalid("layout cursor poisoned"))? = 0;
}
return Ok(NativeResult::Return(Value::Bool(repeat)));
}
// nodes 是先序,逆序测量保证孩子先完成,正序安排保证父 bounds 已经分配到孩子。
let (phase, index) = if position < count {
(Phase::Measure, count - position - 1)
} else {
(Phase::Arrange, position - count)
};
// 只升级当前出现,Step 显式捕获保证这一步的 Mount 活到收集完成,其他候选仍保留弱身份。
let mount = cx
.upgrade(self.mounts[index])?
.ok_or(invalid("layout Mount expired"))?;
// 只为当前出现建立 Step 与 collector,显式 captures 保活本次 Mount/desktop,避免一次分配整棵调用树。
let step = cx.allocate(ContextNativeFunction::new(
"layout step",
Step {
plan: self.plan.clone(),
index,
phase,
mount,
desktop: self.desktop,
},
vec![mount, self.desktop],
))?;
// 收集器保存同一 index/phase,保证方法结果回写正确出现而不依赖驱动游标当前值。
let collector = cx.allocate(ContextNativeFunction::new(
"collect layout",
Collect {
plan: self.plan.clone(),
index,
phase,
desktop: self.desktop,
},
vec![self.desktop],
))?;
// 执行一步并立即收集,再给外层 repeat_call 返回 true;各轮迭代共享原 VM 帧和预算。
let function = crate::vm_sequence::collect_and_continue(
cx,
collector,
step,
vec![],
"layout occurrence",
self.desktop,
)?;
Ok(NativeResult::Invoke {
function,
arguments: vec![],
})
}
}
/// An unpublished layout frame and derived caches. These are merged only after
/// the driver completes; they are not another copy of application state.
/// 未发布布局草稿及派生缓存;Work 持有期间仍在求解,Complete 后交桥接层投影与提交。
pub struct Draft {
// 未投影/未发布的布局草稿;ui_bridge 完成后还要执行滚动、视口投影与挂载提交。
pub frame: Frame,
// 完整求解统计和原生运行时缓存,与 frame、源 Cache 一起交付,不能单独提前覆盖已发布状态。
pub runtime: LayoutRuntime,
pub cache: Cache,
pub reads: Vec<endor_core::ReadDependency>,
}
#[must_use = "pending layout work must be resumed or cancelled with its owning VM"]
/// 本次推进处于挂起还是完成;挂起工作必须继续恢复或显式取消。
pub enum Progress {
// ui_bridge 保存工作句柄和上下文,endor_host 之后驱动恢复或取消,不发布中间几何。
Pending(Work),
Complete(Draft),
}
/// Host-owned continuation. No borrowed frame/cache references cross a pause.
/// The prepare handle owns one root until completion or explicit cancellation.
/// 拥有未完成源调用及临时根的工作句柄;丢弃 Rust 值不能替代 VM 内的取消。
pub struct Work {
// 开始时的原生草稿与缓存状态独立拥有,挂起不携带外部可变引用。
state: Draft,
plan: Shared,
// 驱动入口拥有根,正常完成或 cancel 都需显式 release;Rust drop 不负责 VM 引用计数。
prepare: ObjectRef,
desktop: ObjectRef,
slices: usize,
}
impl Work {
/// 恢复当前 Work 拥有的 VM 续体,继续同一轮求解而不是重放 measure/arrange。
// ui_bridge::resume_layout_frame 调用,沿原只读续体继续;slices 统计包含 start 的第一片。
pub fn resume(mut self, vm: &mut Vm, slice: usize) -> Result<Progress> {
self.slices += 1;
let progress = vm.resume_read_only_call(slice);
self.accept(vm, progress)
}
/// 显式结束布局续体和只读域,释放本次尚未提交的临时根。
// 桥接层替换或撤销挂起布局时调用;消费 Work 结束续体,派生 Plan 不合入文档。
pub fn cancel(self, vm: &mut Vm) -> Result<()> {
// 取消与释放入口根均先尝试,再依次传播错误;普通 Rust drop 无法替代 VM 只读域清理。
let cancelled = vm.cancel_read_only_call();
let released = vm.release(self.prepare);
cancelled?;
released?;
Ok(())
}
/// 接收一次续体进度,挂起时保留工作,完成时将派生结果交给收尾阶段。
fn accept(
self,
vm: &mut Vm,
progress: Result<endor_core::ExecutionProgress, VmError>,
) -> Result<Progress> {
// Pending 保存所有本轮状态;Complete 释放入口后汇总,错误先取诊断再尝试取消清理。
match progress {
Ok(endor_core::ExecutionProgress::Pending { .. }) => Ok(Progress::Pending(self)),
Ok(endor_core::ExecutionProgress::Complete {
value,
dependencies,
..
}) => {
vm.release(self.prepare)?;
finish(vm, self, value, dependencies)
}
Err(error) => {
// An invalid quantum can fail without discarding the VM's
// continuation. No failed job may strand a read-only scope.
let diagnostic = crate::endor_host::runtime_error(vm, error);
self.cancel(vm)?;
Err(diagnostic)
}
}
}
}
/// 建立只读布局 Plan 与托管驱动函数;小型原生树可走直接路径,大树保留分片推进能力。
// ui_bridge::begin_layout_frame 提供本轮结构帧、临时 Mount 与缓存副本,结果由 accept_layout_progress 接续。
pub fn start(
vm: &mut Vm,
desktop: ObjectRef,
mounts: Vec<WeakObjectRef>,
root: Arc<UiNode>,
mut state: Draft,
slice: usize,
) -> Result<Progress> {
// 禁止零切片及同 VM 已有只读执行,不能覆盖另一个挂起续体。
anyhow::ensure!(slice > 0, "layout slice must be positive");
anyhow::ensure!(
!vm.has_suspended_call() && !vm.object_store().is_read_only(),
"cannot start layout while another read-only execution is active"
);
// 分字段借用草稿以准备本轮,cache 命中统计重置但条目先保留供下方筛选。
let frame = &mut state.frame;
let runtime = &mut state.runtime;
let cache = &mut state.cache;
cache.hits = 0;
cache.misses = 0;
// 当前出现弱身份集合用于剪掉退役缓存键;缓存条目内的业务借用身份不会因此增加根。
let live: HashSet<_> = mounts.iter().copied().collect();
// 先删除不再出现在此结构中的测量项,防止已移除的弱 Mount 身份无限积累。
cache.entries.retain(|mount, _| live.contains(mount));
// Preserve the existing native measurement cache and avoid VM/result
// allocation for small trees without custom layout methods. Contexts already exist.
// 扫描每个出现的实际 measure/arrange,实现全部为原生还不够,下面 flex/拉伸也会要求收敛驱动。
let mut custom = false;
for weak in &mounts {
let mount = vm
.upgrade(*weak)?
.ok_or_else(|| anyhow::anyhow!("layout Mount expired"))?;
// 方法检查错误先保存为 Result,下面总会尝试 release 临时升级的 Mount,再传播检查结果。
let result = (|| {
let block = vm.payload::<Mount>(mount, "Mount")?.block;
let definition = vm
.object_store()
.type_of(block)?
.ok_or(invalid("untyped layout target"))?;
// measure 与 arrange 任一源覆盖或缺失都不能走小原生直接路径;缺失在正式步骤中报告协议错误。
for name in ["measure", "arrange"] {
let Some(method) = vm.object_store().lookup_method(definition, name)? else {
return Ok::<_, VmError>(true);
};
if !crate::native_visual::is_intrinsic(vm.object_store(), method, name, desktop)? {
return Ok(true);
}
}
Ok(false)
})();
vm.release(mount)?;
custom |= result?;
}
// 用显式栈建立视觉先序列表,倒序压入孩子保持原兄弟顺序,与结构帧/Mount 的出现顺序对应。
let mut nodes = Vec::new();
let mut work = vec![root.clone()];
// 除了真实源覆盖,flex 或分配孩子的容器也需要收敛驱动,因为安排可能反过来改变测量。
while let Some(node) = work.pop() {
custom |= node.has_flex() || node.allocates_children();
work.extend(node.children().iter().rev().cloned());
nodes.push(node);
}
// Flex allocation can change text wrapping and ancestor measurements even
// when every protocol method is native. Use the same settling driver.
// 仅小于等于64节点且无自定义/弹性分配的树走直接路径;大原生树仍经同一驱动以便分片让出。
if !custom && nodes.len() <= NATIVE_FAST_PATH_MAX_NODES {
// 直接路径不使用源测量缓存;保留的原生测量缓存属于 runtime,清空的是这里的源 Cache。
cache.clear();
// 小原生树一次生成完整帧,无须分配协议结果对象;前面的只读执行排他检查仍然适用。
let mut native = runtime.frame(&root)?;
// Only the structural pass consumes a published version.
// 结构准备已经分配本次发布版本,快速完整测量不能再多消耗一个可见布局版本。
native.layout_version = frame.layout_version;
runtime.version = frame.layout_version;
*frame = native;
state.reads.clear();
return Ok(Progress::Complete(state));
}
// 帧结构、弱 Mount 和视觉节点必须同序同数量,否则按出现下标执行协议会把上下文接错。
anyhow::ensure!(
nodes.len() == mounts.len() && nodes.len() == frame.mounts.len(),
"layout structure does not match Mount contexts"
);
// 按结构帧的 Mount ID 查父出现位置,再构造与 nodes 同下标的孩子索引表。
let indices: HashMap<_, _> = frame
.mounts
.iter()
.enumerate()
.map(|(index, mount)| (mount.id, index))
.collect();
let mut children = vec![Vec::new(); nodes.len()];
// 只记录直接父子关系,根没有父项;协议的子尺寸和稳定 slot 都按这个顺序匹配。
for (index, mount) in frame.mounts.iter().enumerate() {
if let Some(parent) = mount.parent {
children[indices[&parent]].push(index);
}
}
// 从根向下初始化约束,之后每轮由 Settle 结合真实父分配细化;初始值不是最终精确几何。
let mut constraints = vec![runtime.root_constraints(); nodes.len()];
// 父约束已在更早下标准备好,再逐孩子推导首次约束,后续实际分配仍可能触发重测。
for index in 0..nodes.len() {
for (child_index, child) in children[index].iter().enumerate() {
constraints[*child] = layout::initial_child_constraints(
&nodes[index],
&nodes[*child],
constraints[index],
child_index,
);
}
}
// 以下平行数组均按出现数量初始化,None 代表尚未测量/安排或尚无明确轴分配。
let count = nodes.len();
// 把运行时/缓存克隆进未发布 Plan,失败或取消时上层可以丢弃派生求解结果而不提交半帧。
let plan = Arc::new(Mutex::new(Plan {
sizes: vec![Size::default(); nodes.len()],
preferred: vec![None; nodes.len()],
bounds: vec![None; nodes.len()],
nodes,
children,
runtime: runtime.clone(),
custom: false,
cache: cache.clone(),
pending: (0..count).map(|_| None).collect(),
constraints,
allocations: vec![[None, None]; count],
custom_arrange: vec![false; count],
native_arrange: vec![false; count],
custom_measure: vec![false; count],
measured_limits: vec![[None, None]; count],
previous: None,
seen: BTreeSet::new(),
}));
// 共享 Plan 由原生适配器持有,desktop 显式作为托管捕获;Work 保存入口拥有根直到完成/取消。
let prepare = vm.allocate(ContextNativeFunction::new(
"prepare source layout",
Prepare {
plan: plan.clone(),
mounts,
desktop,
},
vec![desktop],
))?;
// 在执行第一片前绑定草稿、计划和入口,第一片完成或挂起都走相同 accept 分支。
let work = Work {
state,
plan,
prepare,
desktop,
slices: 1,
};
// 一次有界只读调用跨所有节点与收敛轮次;slice 只控制让出量,总指令预算仍共享一百万。
let progress = vm.start_read_only_call(
prepare,
&[],
endor_core::ExecutionLimits {
max_frames: 128,
max_instructions: 1_000_000,
},
slice,
);
work.accept(vm, progress)
}
/// 验证求解已收敛,收集最终几何、读取依赖和缓存,返回尚待桥接层提交的 Draft。
// 只由 Work::accept 收到 VM Complete 后调用;驱动已经停止,prepare 根已经释放。
fn finish(
vm: &mut Vm,
mut work: Work,
value: Value,
reads: Vec<endor_core::ReadDependency>,
) -> Result<Progress> {
// 驱动返回值仅为控制流结果,先释放;真正结果保存在原生 Plan,而非返回值中的第二份业务图。
vm.release_value(value)?;
// 借用最终计划并记录实际切片数,接下来只汇总结果,不再执行源布局或重启求解。
let mut plan = work
.plan
.lock()
.map_err(|_| anyhow::anyhow!("layout plan poisoned"))?;
plan.runtime.slices = work.slices;
let frame = &mut work.state.frame;
// 用最终收敛 bounds 更新 frame 的各出现;这里只生成默认内容命令,后续投影/源 paint 还会重建完整绘制表。
frame.commands.clear();
// 每次出现必须有最终 bounds,否则拒绝把缺失安排的草稿交给桥接层。
for (index, mount) in frame.mounts.iter_mut().enumerate() {
let [x, y, width, height] =
plan.bounds[index].ok_or_else(|| anyhow::anyhow!("unarranged occurrence"))?;
mount.x = x;
mount.y = y;
mount.width = width;
mount.height = height;
// 仅生成可用的默认单条绘制并检查有效性;完整 paint 与平行元数据在桥接投影/源绘制阶段重建。
if let Some(command) =
layout::paint_native(&plan.nodes[index], x, y, Size { width, height })
{
command.validate()?;
frame.commands.push(command);
}
}
// 同步取回最终运行时统计和源测量缓存,避免成功帧搭配上轮缓存。
work.state.runtime = plan.runtime.clone();
work.state.cache = plan.cache.clone();
// 大原生树可能使用分片驱动但没有真正源读取,因此无需把宿主驱动读取当业务布局依赖发布。
if !plan.custom {
work.state.reads.clear();
return Ok(Progress::Complete(work.state));
}
// 源布局存在时才过滤真实读取;类型注册表属于宿主内部协议检查,不能作为业务失效依赖。
let registry = match vm.get_attr(work.desktop, "types")? {
Value::Object(object) => object,
_ => anyhow::bail!("missing type registry"),
};
work.state.reads = reads
.into_iter()
.filter(|dependency| {
// 剔除桌面、注册表、Mount 和已死亡身份,剩余依赖交 ui_bridge 做持续布局失效判断。
let object = dependency.object();
object != work.desktop
&& object != registry
&& vm
.object(object)
.is_ok_and(|object| !object.payload().as_any().is::<Mount>())
})
.collect();
Ok(Progress::Complete(work.state))
}