//! 源 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. //! //! ui_bridge::begin_layout_frame 先建立临时 Mount 上下文和检查点,再调用 start;本模块只拥有求解草稿与 VM 续体。 //! Prepare 建立循环,Advance 依次安排 Step → 源方法/原生结果 → Collect,整轮结束由 Settle 决定继续还是停止。 //! Progress::Complete 只证明本轮布局驱动完成,桥接层仍需投影、提交几何及生命周期,运行时还需绘制并验证全窗口稳定。 //! Work::cancel 结束只读调用并释放入口根;恢复 Mount 检查点属于桥接层,不能把本模块取消误认为业务状态回滚。 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 { 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, // [最小宽,最小高,最大宽,最大高];相同 Block 在不同父约束下不能共用旧测量。 constraints: [f32; 4], // 已按出现约束钳制的最终测量输出,不保存源返回对象或为它增加拥有根。 size: Size, // 源调用及返回字段解码的精确读取依赖,Step 逐项检查版本/名称有效性后才复用。 reads: Vec, } /// 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, // start 每轮清零的源测量缓存统计,原生 LayoutRuntime 自己维护另一套测量记账。 pub hits: usize, pub misses: usize, } impl Cache { /// 从测量缓存依赖中移除宿主私有 desktop 记账对象,防止内部同步造成自我失效。 // ui_bridge 合入缓存前传入全部窗口私有 desktop,防止另一窗口宿主记账也成为自我失效依赖。 pub(crate) fn exclude_host_desktops(&mut self, desktops: &HashSet) { // 逐测量项过滤依赖,保留缓存输入与尺寸不变;这里只排除宿主对象,不重新执行 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) { 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, constraints: [f32; 4], // 本次出现弱身份,测量成功后作为 Cache 键;Pending 本身不额外保活 Mount。 key: WeakObjectRef, } #[derive(Debug)] /// 未发布的布局求解工作区,包含出现树、期望尺寸、分配边界、收敛记录和缓存。 struct Plan { // 使用出现索引保存树和几何,不为每个位置建立第二份业务对象。 nodes: Vec>, // 按出现下标保存直接孩子列表,和 nodes/sizes/bounds 一起形成平行数组。 children: Vec>, // 每一轮最新测量值;preferred 则保留自然基准,避免弹性分配反馈后不断漂移。 sizes: Vec, // preferred 是测量偏好,bounds 是父级实际分配;换行/flex 会使两者不同。 preferred: Vec>, bounds: Vec>, // 本轮的原生测量运行时与统计副本,完成后才随 Draft 交回桥接层。 runtime: LayoutRuntime, // 是否实际遇到源布局覆盖;大原生树也走驱动,但不因此发布宿主驱动读取为业务依赖。 custom: bool, cache: Cache, // 只为真正执行的源 measure 保存收集状态;原生结果或缓存命中不打开新跟踪区间。 pending: Vec>, constraints: Vec<[f32; 4]>, // Source-arranged dimensions remain exact for this solve, not business state. // 每个出现两个轴的显式分配,Some 形成精确 min=max 约束;不是可写业务尺寸属性。 allocations: Vec<[Option; 2]>, // 源 arrange 或需要 flex/stretch 的原生安排会记录显式孩子分配,供 Settle 反推精确约束。 custom_arrange: Vec, native_arrange: Vec, custom_measure: Vec, // 源 measure 与默认结果不同的轴作为本次求解的内容上限;新一轮 start 会重新初始化。 measured_limits: Vec<[Option; 2]>, // 保存几何状态用于检测收敛和重复状态,防止相互依赖布局无限循环。 previous: Option>, seen: BTreeSet>, } // Step/Collect/Settle 共享纯原生求解计划;锁用于短时读写,源 VM 方法不在持锁期间执行。 type Shared = Arc>; #[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 { Some(endor_core::CallArity::exact(0)) } /// 解析当前实际 measure/arrange 实现,决定原生快速路径或源调用,准备本节点输入及依赖收集,避免跳过源覆盖。 fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result { // 先克隆 Mount 描述以跨后续 VM 分配使用;它带当前 Block 和临时布局上下文。 let mount = cx.store().payload::(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::>(); // preferred 优先取本轮最初测量值,不能将上一轮 flex 分配当作新的自然基准,造成累计漂移。 let preferred = plan.children[self.index] .iter() .map(|child| plan.preferred[*child].unwrap_or(plan.sizes[*child])) .collect::>(); // 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::().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"))?; { // 只更新当前出现的临时协议输入;不写 Plan 的兄弟位置,也不修改共享 Block 的业务属性。 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 { Some(endor_core::CallArity::exact(1)) } /// 结束当前测量读跟踪并解码结果,验证尺寸或完整子槽安排,再更新计划与可复用测量依赖。 fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result { // 取走本节点待收集状态,确保每个跟踪区间只结束一次;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; // 类型字典用于协议名义检查,不是源业务状态;它和 desktop 都要从易变性判断与缓存依赖中排除。 let registry = desktop_type_registry(cx, self.desktop)?; // accesses 提供内容读取细节,dependencies 则用于后面保留可校验依赖,两者用途不同。 for read in &reads.accesses { // access 记录哪个对象被怎样读取,后面仅对真实 Mount 内容读取禁用缓存,不因普通身份比较一概禁用。 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 身份可用于查父关系,但内容读取可能涉及本轮派生几何;content_read 才决定此次测量是否易变。 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::() }) }) .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)?; // 固定当前父出现的直接孩子列表,源结果按 slot 查回这个列表,再转换为全局出现下标。 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(); // 列表每项都需满足 Bounds 名义类型,不能只有 x/y/width/height 同名字段就当作合法安排。 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 { // Bounds 名义类型合法仍不足以接受任意 slot 值,缺失或非 Number 要在建立孩子映射之前拒绝。 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 { Some(endor_core::CallArity::exact(0)) } /// 由新 bounds 推导下一轮约束与自然尺寸,比较完整布局状态;稳定则结束,循环状态报错,否则继续同帧求解。 fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result { // 收敛计算持 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() { // 克隆直接孩子索引以结束对 plan.children 的借用,随后要在同一 Plan 内更新约束与显式分配。 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] { // 源测量给出的纵向内容限制只覆盖最大高,最小约束仍从 next 继承,再由孩子约束策略统一处理。 available[3] = height; } // 父级实际分配与测量不同,孩子的可用宽度应跟随分配,否则换行仍按旧宽度计算。 if width != plan.sizes[index].width { available[2] = width; } if height != plan.sizes[index].height { // 最终分配高度与测高不同,则孩子预算跟随实际分配;不把此值写回业务对象 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, ); // 从自然约束起步,再逐轴套入显式精确分配;natural 留作原生 flex 分配失效时恢复基线。 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] { // 显式高同时成为 minHeight/maxHeight;这比仅给上限更强,使下一轮按安排分配重新测量。 constraints[1] = height; constraints[3] = height; } // 线性布局才有主/交叉轴;交叉轴范围变化可触发文字重排,进而改变主轴自然尺寸。 if let Some(main) = layout::main_axis(&plan.nodes[index]) { // 轴索引只有 0/1;Row 主轴横向、Column 主轴纵向,所以用 1-main 得到另一轴而非孩子顺序。 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")?.layout { // 仍有临时上下文才清 bounds;其余输入留给下一轮 Step 更新,不在这里调用源 measure。 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, desktop: ObjectRef, } impl NativeCallable for Prepare { /// 布局驱动的零参入口,Mount 列表/desktop/计划已由调用方捕获。 fn arity(&self) -> Option { Some(endor_core::CallArity::exact(0)) } /// 建立按出现位置推进的 Advance 和固定字节码循环,保持同一 VM 预算,避免首次让出前创建整树临时调用对象。 fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result { // 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], ))?; // 固定循环以 Advance 返回的 Bool 决定继续,不为每个节点展开另一份巨大的驱动字节码。 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, } impl NativeCallable for Advance { /// 由共享布局循环无参调用,游标表示本轮测量/排列的位置。 fn arity(&self) -> Option { Some(endor_core::CallArity::exact(0)) } /// 按游标安排自底向上 measure 与自顶向下 arrange,短锁结束后才创建/执行当前节点调用,末尾进入收敛检查。 fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result { // 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 { // Settle 是宿主内部零参计算,必须同步返回 Bool;其他结果不能误作“已经稳定”而提前交付。 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, } #[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 { // resume 消费原 Work,计数包含最初 start;非法量子/恢复错误同样交 accept 取消原只读续体,不重建求解。 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, ) -> Result { // Pending 保存所有本轮状态;Complete 释放入口后汇总,错误先取诊断再尝试取消清理。 match progress { Ok(endor_core::ExecutionProgress::Pending { .. }) => Ok(Progress::Pending(self)), Ok(endor_core::ExecutionProgress::Complete { value, dependencies, .. }) => { // VM 调用域已经完成,先释放其入口根;finish 消费同一个 Work,并只读取最终 Plan 汇总布局草稿。 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. // 在取消调用可能清理 VM 执行上下文之前提取原始 traceback,避免最后只报告清理阶段位置。 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, root: Arc, mut state: Draft, slice: usize, ) -> Result { // 禁止零切片及同 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; // 与 frame/runtime 借用同一 Draft 的独立字段,重置的是本轮源测量统计,不是清掉全部仍可复用的条目。 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 { // 扫描只为选择驱动路径,临时升级保证方法身份检查期间 Mount 不被回收;下面无论检查结果如何都 release。 let mount = vm .upgrade(*weak)? .ok_or_else(|| anyhow::anyhow!("layout Mount expired"))?; // 方法检查错误先保存为 Result,下面总会尝试 release 临时升级的 Mount,再传播检查结果。 let result = (|| { let block = vm.payload::(mount, "Mount")?.block; // 从真实业务对象取得类型,不能使用 UiNode 种类推断源覆盖是否存在。 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)? { // 任一协议为源覆盖就需要 VM 驱动,不能因另一协议仍是原生而整体走直接布局。 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, ) -> Result { // 驱动返回值仅为控制流结果,先释放;真正结果保存在原生 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::()) }) .collect(); Ok(Progress::Complete(work.state)) }