docs: clarify layout continuation and runtime refresh cleanup

This commit is contained in:
2026-11-12 20:09:27 +08:00
parent f1e413c8c5
commit b10a5b43d2
2 changed files with 47 additions and 1 deletions
+30
View File
@@ -4,6 +4,11 @@
//!
//! 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},
@@ -289,6 +294,7 @@ impl NativeCallable for Step {
// 先安装本轮节点、子测量和临时 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"))?;
@@ -448,6 +454,7 @@ impl NativeCallable for Collect {
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);
}
}
@@ -465,9 +472,11 @@ impl NativeCallable for Collect {
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 {
@@ -477,6 +486,7 @@ impl NativeCallable for Collect {
// source. Object-granular tracking cannot prove those
// accesses stable, so conservatively bypass its cache.
if cx.store().object(object)?.payload().as_any().is::<Mount>() {
// Mount 身份可用于查父关系,但内容读取可能涉及本轮派生几何;content_read 才决定此次测量是否易变。
volatile |= read.content_read;
continue;
}
@@ -524,6 +534,7 @@ impl NativeCallable for Collect {
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() {
@@ -541,6 +552,7 @@ impl NativeCallable for Collect {
.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 {
@@ -549,6 +561,7 @@ impl NativeCallable for Collect {
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 范围的值也不能作为此处身份。
@@ -596,6 +609,7 @@ impl NativeCallable for Collect {
plan.allocations[child][0] = Some(bounds[2]);
}
if bounds[3] != plan.sizes[child].height {
// 高度差异单独形成精确纵向分配,和上面的宽度约束可同时存在,也可只约束其中一轴。
plan.allocations[child][1] = Some(bounds[3]);
}
}
@@ -631,6 +645,7 @@ impl NativeCallable for Settle {
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"))?;
@@ -641,6 +656,7 @@ impl NativeCallable for Settle {
available[2] = width;
}
if let Some(height) = plan.measured_limits[index][1] {
// 源测量给出的纵向内容限制只覆盖最大高,最小约束仍从 next 继承,再由孩子约束策略统一处理。
available[3] = height;
}
// 父级实际分配与测量不同,孩子的可用宽度应跟随分配,否则换行仍按旧宽度计算。
@@ -648,6 +664,7 @@ impl NativeCallable for Settle {
available[2] = width;
}
if height != plan.sizes[index].height {
// 最终分配高度与测高不同,则孩子预算跟随实际分配;不把此值写回业务对象 height。
available[3] = height;
}
// 根据父盒模型与实际宽高计算孩子公共约束基础,随后再考虑各个出现的独立尺寸规则。
@@ -666,6 +683,7 @@ impl NativeCallable for Settle {
Size { width, height },
child_index,
);
// 从自然约束起步,再逐轴套入显式精确分配;natural 留作原生 flex 分配失效时恢复基线。
let mut constraints = natural;
// 显式轴分配同时设置最小与最大,区别于仅给孩子一个可自由缩小的最大可用宽度。
if let Some(width) = plan.allocations[child][0] {
@@ -673,11 +691,13 @@ impl NativeCallable for Settle {
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]
@@ -752,6 +772,7 @@ impl NativeCallable for Settle {
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 {
// 仍有临时上下文才清 bounds;其余输入留给下一轮 Step 更新,不在这里调用源 measure。
context
.lock()
.map_err(|_| invalid("layout context poisoned"))?
@@ -788,6 +809,7 @@ impl NativeCallable for Prepare {
},
vec![self.desktop],
))?;
// 固定循环以 Advance 返回的 Bool 决定继续,不为每个节点展开另一份巨大的驱动字节码。
let function = crate::vm_sequence::repeat_call(cx, advance, "source layout", self.desktop)?;
Ok(NativeResult::Invoke {
function,
@@ -836,6 +858,7 @@ impl NativeCallable for Advance {
}
.invoke(cx, &[])?;
let NativeResult::Return(Value::Bool(repeat)) = settled else {
// Settle 是宿主内部零参计算,必须同步返回 Bool;其他结果不能误作“已经稳定”而提前交付。
return Err(invalid("invalid settling result"));
};
// 需要继续时重置同一个游标,后续仍回到原 repeat_call 帧执行整轮;false 则结束布局。
@@ -933,6 +956,7 @@ 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> {
// resume 消费原 Work,计数包含最初 start;非法量子/恢复错误同样交 accept 取消原只读续体,不重建求解。
self.slices += 1;
let progress = vm.resume_read_only_call(slice);
self.accept(vm, progress)
@@ -963,12 +987,14 @@ impl Work {
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)
@@ -996,6 +1022,7 @@ pub fn start(
// 分字段借用草稿以准备本轮,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;
@@ -1008,12 +1035,14 @@ pub fn start(
// 扫描每个出现的实际 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, "Mount")?.block;
// 从真实业务对象取得类型,不能使用 UiNode 种类推断源覆盖是否存在。
let definition = vm
.object_store()
.type_of(block)?
@@ -1024,6 +1053,7 @@ pub fn start(
return Ok::<_, VmError>(true);
};
if !crate::native_visual::is_intrinsic(vm.object_store(), method, name, desktop)? {
// 任一协议为源覆盖就需要 VM 驱动,不能因另一协议仍是原生而整体走直接布局。
return Ok(true);
}
}