307 lines
16 KiB
Rust
307 lines
16 KiB
Rust
//! 一个原生窗口对应一份 Presentation:独立视口、Mount、测量缓存、焦点与依赖记录。
|
||
//! 多个窗口仍使用同一个 VM 和共享业务对象。读取版本失效时只标记相关展示需要刷新,不能因此复制一份业务状态。
|
||
//!
|
||
//! 调用路径:bootstrap/新窗口入口创建 Presentation;UiBridge 收集 VM changes 后对每窗口调用 observe_changes。
|
||
//! Block.invalidate 的桥接路径调用 invalidate,源布局/绘制完成后用 split_reads 保存本窗口实际读取。
|
||
//! 本结构是 VM 线程内宿主状态,不能把其借用 ObjectRef 或弱表直接交给窗口线程操作。
|
||
//!
|
||
//! Per-window rendering state. Business objects and code remain in one VM.
|
||
//! Registries and caches borrow checked identities; the managed desktop owns
|
||
//! presentation roots through its pending/mount dictionaries.
|
||
use crate::ui_bridge::UiNode;
|
||
use endor_core::{ChangeBatch, NativeContext, ObjectRef, Vm, VmError, WeakObjectRef};
|
||
use std::sync::Arc;
|
||
|
||
#[derive(Debug)]
|
||
/// 一个窗口独立的展示状态:活动页面、待提交请求、Mount、布局与源读取缓存;共享 VM 的业务状态不放在这里复制。
|
||
pub(crate) struct Presentation {
|
||
// 所属托管桌面身份;此结构借用身份,真正的展示根由托管桌面字典维持。
|
||
pub desktop: ObjectRef,
|
||
// 窗口正在退役的标记,阻止常规新展示请求在关闭阶段复活旧上下文。
|
||
pub closing: bool,
|
||
// 当前窗口标签字典的借用身份,真正拥有边由托管 desktop 保持。
|
||
pub tabs: ObjectRef,
|
||
// 当前已生效 Page 的借用身份;页面生命期仍由托管展示根维持。
|
||
pub active: Option<ObjectRef>,
|
||
// 双层 Option 区分三态:None 无请求,Some(None) 请求清空,Some(Some(page)) 请求切页。
|
||
pub pending: Option<Option<ObjectRef>>,
|
||
// 最近原生根快照,比较 Arc/内容以决定是否需要新的布局,不强持有 VM 业务对象。
|
||
pub last_snapshot: Option<Arc<UiNode>>,
|
||
// 弱身份到原生稳定标签 ID 的映射;不能因为标签历史而保活已经关闭的页面。
|
||
pub tab_ids: Vec<(WeakObjectRef, u64)>,
|
||
// 分配新原生标签 ID 的单调计数,与可被回收的 Page 弱身份分开。
|
||
pub next_tab: u64,
|
||
// 最近发布的标签值列表,用于检测标题/活动项/顺序变化。
|
||
pub last_tabs: Vec<crate::tabs::Tab>,
|
||
// 当前页面兼容表面相机和模式,随本窗口展示更新。
|
||
pub surface: crate::page::Surface,
|
||
// 托管业务图到原生视觉树的缓存及反向依赖,不能替代源测量缓存。
|
||
pub render_cache: crate::snapshot::RenderCache,
|
||
// 本窗口几何求解、出现身份与布局缓存,多个窗口共享业务树但不共享此状态。
|
||
pub layout: crate::layout::LayoutRuntime,
|
||
// 本窗口活动 Mount 的弱索引与发布顺序,托管字典负责强拥有。
|
||
pub mounts: crate::mount::MountTable,
|
||
// 最后完整发布的布局版本,未提交的分片结果不能直接覆盖此边界。
|
||
pub published_version: u64,
|
||
// 即使根快照身份未变也要求重新发布一帧,例如源绘制读取的数据发生变化时。
|
||
pub force_frame: bool,
|
||
// 精确名称/字典种类依赖,与下面按整个对象版本记录的 reads 分开检查。
|
||
pub paint_names: Vec<endor_core::ReadDependency>,
|
||
// 源布局精确名字/字典类别读取,与 layout_reads 的整对象版本分别检查。
|
||
pub layout_names: Vec<endor_core::ReadDependency>,
|
||
// 源 paint 的对象内容版本观察,仅借用身份,不 retain 对象。
|
||
pub paint_reads: std::collections::BTreeMap<ObjectRef, u64>,
|
||
// 观察到读取失效的累计次数,饱和累加用于诊断,不作为实际刷新次数。
|
||
pub dependency_invalidations: usize,
|
||
// 最近失效的阶段/对象或名字诊断;同次两类检查都命中时后者覆盖前者。
|
||
pub last_invalid_dependency: String,
|
||
// 源 measure/arrange 实际读取的整对象版本,更新后按该版本观察决定刷新。
|
||
pub layout_reads: std::collections::BTreeMap<ObjectRef, u64>,
|
||
// 源测量的参数/依赖缓存,paint-only 显式失效不清除此缓存。
|
||
pub measure_cache: crate::source_layout::Cache,
|
||
// 最近快照提取实际重建节点统计,供开发工具解释刷新成本。
|
||
pub snapshot_visited: usize,
|
||
// 最近快照提取直接复用条目统计,与 visited 分开展示。
|
||
pub snapshot_reused: usize,
|
||
}
|
||
|
||
impl Presentation {
|
||
/// 比较布局/绘制读取版本和名称依赖,标记需要刷新并通知快照缓存。
|
||
pub fn observe_changes(&mut self, vm: &Vm, changes: &ChangeBatch) {
|
||
// 先检查粗粒度对象版本,layout 优先于 paint;每类只记录第一个失效项用于诊断。
|
||
// 对象已经释放时版本查询失败,也视作失效,而非继续使用旧缓存。
|
||
if let Some((phase, object, version)) = self
|
||
.layout_reads
|
||
.iter()
|
||
.map(|(object, version)| ("layout", object, version))
|
||
.chain(
|
||
self.paint_reads
|
||
.iter()
|
||
.map(|(object, version)| ("paint", object, version)),
|
||
)
|
||
.find(|(_, object, version)| vm.change_version(**object).ok() != Some(**version))
|
||
{
|
||
self.force_frame = true;
|
||
self.dependency_invalidations = self.dependency_invalidations.saturating_add(1);
|
||
// 失效诊断尽量附 payload 种类;对象已释放时用 released,而不是解引用过期句柄。
|
||
let kind = vm
|
||
.object(*object)
|
||
.map(|value| value.payload().kind_name())
|
||
.unwrap_or("released");
|
||
self.last_invalid_dependency = format!(
|
||
"{phase} {kind} {object}: {version} -> {:?}",
|
||
vm.change_version(*object).ok()
|
||
);
|
||
}
|
||
// 再核对名称级依赖,避免仅凭对象版本漏掉名称解析路径上的变化。
|
||
// 两个检查独立执行,因此同次观察可能分别增加计数,后者诊断可覆盖前者。
|
||
if let Some((phase, dependency)) = self
|
||
.layout_names
|
||
.iter()
|
||
.map(|dependency| ("layout", dependency))
|
||
.chain(
|
||
self.paint_names
|
||
.iter()
|
||
.map(|dependency| ("paint", dependency)),
|
||
)
|
||
.find(|(_, dependency)| !vm.object_store().read_dependency_valid(dependency))
|
||
{
|
||
self.force_frame = true;
|
||
self.dependency_invalidations = self.dependency_invalidations.saturating_add(1);
|
||
// Name 依赖显示具体名字,其他精确记录显示字典类别变化,不假装都有属性名。
|
||
let name = match dependency {
|
||
// 名字解析路径中的具体名称用于调试哪些全局/属性访问使布局或绘制失效。
|
||
endor_core::ReadDependency::Name { name, .. } => name.as_str(),
|
||
// 非 Name 分支表示字典种类观察,仍需保留其失效事实。
|
||
_ => "<dictionary kind>",
|
||
};
|
||
self.last_invalid_dependency = format!("{phase} name {}:{name}", dependency.object());
|
||
}
|
||
// 把同一批 VM 写入交给视觉快照缓存,源布局/绘制依赖与原生快照依赖分别维护。
|
||
self.render_cache.invalidate(ChangeBatch {
|
||
changes: changes.changes.clone(),
|
||
rescan_required: changes.rescan_required,
|
||
});
|
||
}
|
||
|
||
/// 按尺寸、内容或绘制失效原因清理对应缓存,避免把所有变化都当成相同重建。
|
||
pub fn invalidate(
|
||
&mut self,
|
||
cx: &mut NativeContext<'_>,
|
||
affected: &std::collections::HashSet<ObjectRef>,
|
||
reason: &str,
|
||
) -> Result<(), VmError> {
|
||
// 仅绘制失效不影响测量;其余原因清除测量结果并按 Block ID 使布局缓存失效。
|
||
if reason != "paint" {
|
||
self.measure_cache.invalidate(affected);
|
||
// 源测量缓存按业务身份失效,布局缓存则使用其身份文本;同一窗口所有受影响对象都处理。
|
||
for block in affected {
|
||
self.layout.invalidate(&block.to_string());
|
||
}
|
||
}
|
||
// 内容变化还可能改变组成结构,因此显式失效视觉快照及其父链;其他原因不额外做此传播。
|
||
if reason == "content" {
|
||
self.render_cache
|
||
.invalidate_objects(affected.iter().copied());
|
||
}
|
||
// 仅当受影响 Block 在该窗口有活动出现时强制刷新,避免无关窗口重复发帧。
|
||
// 逐个弱升级本窗口活动出现,只在此窗口确有受影响 Block 时 force_frame。
|
||
for weak in self.mounts.active.values() {
|
||
// 已消失的 Mount 不触发刷新;升级成功后检查它的业务 Block 是否属于 affected。
|
||
if let Some(mount) = cx.upgrade(*weak)?
|
||
&& affected.contains(
|
||
&cx.store()
|
||
.payload::<crate::mount::Mount>(mount, "Mount")?
|
||
.block,
|
||
)
|
||
{
|
||
self.force_frame = true;
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// 用桌面与标签注册表身份初始化窗口状态,布局和视觉缓存从空状态开始。
|
||
// 这里只初始化窗口级缓存,不创建 VM、不复制业务对象;active/pending 留空等待窗口展示请求。
|
||
pub fn new(desktop: ObjectRef, tabs: ObjectRef) -> Self {
|
||
// 这里只初始化本窗口宿主状态;不分配新的 VM 或业务对象,尚无页面、根快照和读观察。
|
||
Self {
|
||
desktop,
|
||
closing: false,
|
||
tabs,
|
||
active: None,
|
||
pending: None,
|
||
last_snapshot: None,
|
||
tab_ids: Vec::new(),
|
||
next_tab: 0,
|
||
last_tabs: Vec::new(),
|
||
surface: crate::page::Surface::default(),
|
||
render_cache: crate::snapshot::RenderCache::default(),
|
||
layout: crate::layout::LayoutRuntime::default(),
|
||
mounts: crate::mount::MountTable::default(),
|
||
published_version: 0,
|
||
force_frame: false,
|
||
paint_names: Vec::new(),
|
||
layout_names: Vec::new(),
|
||
paint_reads: std::collections::BTreeMap::new(),
|
||
dependency_invalidations: 0,
|
||
last_invalid_dependency: String::new(),
|
||
layout_reads: std::collections::BTreeMap::new(),
|
||
measure_cache: crate::source_layout::Cache::default(),
|
||
snapshot_visited: 0,
|
||
snapshot_reused: 0,
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 把读取记录拆成对象版本和精确名称依赖,便于按实际观察内容判断失效。
|
||
pub(crate) fn split_reads(
|
||
reads: Vec<endor_core::ReadDependency>,
|
||
) -> (
|
||
std::collections::BTreeMap<ObjectRef, u64>,
|
||
Vec<endor_core::ReadDependency>,
|
||
) {
|
||
// 整体对象观察按身份归并到有序表,后续 observe_changes 比较该对象的当前版本。
|
||
let mut objects = std::collections::BTreeMap::new();
|
||
// 其他精确依赖保持原枚举与顺序,不能降级为对象版本而丢掉名字解析语义。
|
||
let mut names = Vec::new();
|
||
// 整对象依赖按身份收集到有序表,重复项以最后一次版本为准;其余精确依赖保持原记录。
|
||
for dependency in reads {
|
||
// 按 ReadDependency 种类拆分;不会 retain 借用身份或执行源函数。
|
||
match dependency {
|
||
// 同对象重复观察以最后出现的版本覆盖先前版本,与本轮实际观察结束状态一致。
|
||
endor_core::ReadDependency::Object { object, version } => {
|
||
objects.insert(object, version);
|
||
}
|
||
// Name/字典类别等精确记录完整保留,交给 ObjectStore 的有效性检查器解释。
|
||
dependency => names.push(dependency),
|
||
}
|
||
}
|
||
(objects, names)
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use crate::{layout::Size, ui_bridge::UiKind};
|
||
#[test]
|
||
/// 同VM中两Presentation分别布局共享树,尺寸/出现身份独立;读观察可追踪共享对象但不能增添GC根。
|
||
fn independent_view_geometry_and_shared_vm_reads_do_not_add_roots() {
|
||
// 测试在同一 VM 中建立两个 Presentation,验证各自布局尺寸/出现 ID 独立,读取依赖共享业务对象。
|
||
let mut vm = Vm::new().unwrap();
|
||
// 第一窗口以独立 desktop/tabs 与独立 LayoutRuntime 初始化,但仍属于同一个 VM。
|
||
let mut left = Presentation::new(vm.new_object().unwrap(), vm.new_dict().unwrap());
|
||
// 第二窗口使用另一套展示状态,随后给它不同有限视口,不能靠复制业务树隔离几何。
|
||
let mut right = Presentation::new(vm.new_object().unwrap(), vm.new_dict().unwrap());
|
||
// 左窗口设 100×80,右窗口设 200×160,同一自然 500×500 根会被各自约束。
|
||
left.layout.viewport = Some(Size {
|
||
width: 100.0,
|
||
height: 80.0,
|
||
});
|
||
right.layout.viewport = Some(Size {
|
||
width: 200.0,
|
||
height: 160.0,
|
||
});
|
||
// 手工不可变原生矩形快照以同一 block_id 供两窗口布局,专门隔离展示几何状态。
|
||
let root = UiNode {
|
||
surface: None,
|
||
flex: 0.0,
|
||
flex_policy: crate::layout::FlexPolicy::default(),
|
||
main_align: crate::layout::MainAlign::Start,
|
||
cross_align: crate::layout::CrossAlign::Start,
|
||
align_self: None,
|
||
margin: crate::layout::Insets::default(),
|
||
sizing: crate::box_model::Sizing::default(),
|
||
padding_edges: [None; 4],
|
||
border: crate::layout::Insets::default(),
|
||
border_color: "#D8DCE2".into(),
|
||
clip_children: false,
|
||
block_id: "shared".into(),
|
||
protocol_key: "rect".into(),
|
||
slots: Vec::new(),
|
||
interactive: false,
|
||
pointer_events: false,
|
||
kind: UiKind::Rect {
|
||
width: 500.0,
|
||
height: 500.0,
|
||
radius: 0.0,
|
||
color: "#237A45".into(),
|
||
},
|
||
};
|
||
// 分别执行真实布局,验证共享快照在左右窗口产生不同分配宽度。
|
||
let left_frame = left.layout.frame(&root).unwrap();
|
||
// 右窗口也读取同一 root,但不应沿用左侧出现身份或约束结果。
|
||
let right_frame = right.layout.frame(&root).unwrap();
|
||
assert_eq!(left_frame.mounts[0].width, 100.0);
|
||
assert_eq!(right_frame.mounts[0].width, 200.0);
|
||
// 只清左窗口出现,下一帧应分配新左 ID,而右窗口不受影响。
|
||
left.layout.clear_occurrences();
|
||
// 新左帧应更换出现 ID,验证 clear_occurrences 的实际效果。
|
||
let new_left = left.layout.frame(&root).unwrap();
|
||
// 未清理的右窗口保留原 ID,说明身份序列/缓存没有跨 Presentation 串用。
|
||
let same_right = right.layout.frame(&root).unwrap();
|
||
assert_ne!(new_left.mounts[0].id, left_frame.mounts[0].id);
|
||
assert_eq!(same_right.mounts[0].id, right_frame.mounts[0].id);
|
||
// 在 VM 中创建共享业务读取源,让两窗口记录不同阶段对同一对象的依赖。
|
||
let shared = vm.new_object().unwrap();
|
||
// 保存写入前的版本;left.paint_reads 与 right.layout_reads 都只借用该身份。
|
||
let version = vm.change_version(shared).unwrap();
|
||
left.paint_reads.insert(shared, version);
|
||
right.layout_reads.insert(shared, version);
|
||
// 实际写入使对象版本改变;两类读取观察都应设置各自 force_frame。
|
||
vm.set_attr(shared, "value", endor_core::Value::Bool(true))
|
||
.unwrap();
|
||
// 同一 VM 变化批次送给两窗口,记录克隆不能制造业务对象副本或新的 GC 根。
|
||
let changes = vm.take_changes();
|
||
left.observe_changes(&vm, &changes);
|
||
right.observe_changes(&vm, &changes);
|
||
assert!(left.force_frame && right.force_frame);
|
||
// 最后释放业务对象并回收:两份读取记录都不应成为托管根,弱引用应升级失败。
|
||
// 只保留弱句柄再释放业务根,最后 collect_cycles 后应无法升级,证明 reads 不保活源对象。
|
||
let weak = vm.downgrade(shared).unwrap();
|
||
vm.release(shared).unwrap();
|
||
vm.collect_cycles();
|
||
assert!(vm.upgrade(weak).unwrap().is_none());
|
||
}
|
||
}
|