//! 一个原生窗口对应一份 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, // 双层 Option 区分三态:None 无请求,Some(None) 请求清空,Some(Some(page)) 请求切页。 pub pending: Option>, // 最近原生根快照,比较 Arc/内容以决定是否需要新的布局,不强持有 VM 业务对象。 pub last_snapshot: Option>, // 弱身份到原生稳定标签 ID 的映射;不能因为标签历史而保活已经关闭的页面。 pub tab_ids: Vec<(WeakObjectRef, u64)>, // 分配新原生标签 ID 的单调计数,与可被回收的 Page 弱身份分开。 pub next_tab: u64, // 最近一次桥接 commit_frame 保存的标签草稿,用于检测标题/活动项/顺序变化;不保证 GUI 已收到该帧。 pub last_tabs: Vec, // 当前页面兼容表面相机和模式,随本窗口展示更新。 pub surface: crate::page::Surface, // 托管业务图到原生视觉树的缓存及反向依赖,不能替代源测量缓存。 pub render_cache: crate::snapshot::RenderCache, // 本窗口几何求解、出现身份与布局缓存,多个窗口共享业务树但不共享此状态。 pub layout: crate::layout::LayoutRuntime, // 本窗口活动 Mount 的弱索引与发布顺序,托管字典负责强拥有。 pub mounts: crate::mount::MountTable, // commit_frame 已同步 Mount 的布局版本,输入检查据此拒绝旧几何;后续 paint/稳定发布仍可能尚未完成。 // 字段名沿用 published_version,但不代表此值已经写入 GUI 共享文档,也不是整个源输入的成功标记。 pub published_version: u64, // 即使根快照身份未变也要求重新发布一帧,例如源绘制读取的数据发生变化时。 pub force_frame: bool, // 精确名称/字典种类依赖,与下面按整个对象版本记录的 reads 分开检查。 pub paint_names: Vec, // 源布局精确名字/字典类别读取,与 layout_reads 的整对象版本分别检查。 pub layout_names: Vec, // 源 paint 的对象内容版本观察,仅借用身份,不 retain 对象。 pub paint_reads: std::collections::BTreeMap, // 观察到读取失效的累计次数,饱和累加用于诊断,不作为实际刷新次数。 pub dependency_invalidations: usize, // 最近失效的阶段/对象或名字诊断;同次两类检查都命中时后者覆盖前者。 pub last_invalid_dependency: String, // 源 measure/arrange 实际读取的整对象版本,更新后按该版本观察决定刷新。 pub layout_reads: std::collections::BTreeMap, // 源测量的参数/依赖缓存,paint-only 显式失效不清除此缓存。 pub measure_cache: crate::source_layout::Cache, // 最近快照提取实际重建节点统计,供开发工具解释刷新成本。 pub snapshot_visited: usize, // 最近快照提取直接复用条目统计,与 visited 分开展示。 pub snapshot_reused: usize, } impl Presentation { /// 比较布局/绘制读取版本和名称依赖,标记需要刷新并通知快照缓存。 /// UiBridge::prepare_frame 将同一 ChangeBatch 广播所有窗口;这里只设置失效状态,实际提取/布局由各窗随后推进。 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() ); } // 再核对精确依赖,既包括对象属性名,也包括命名空间查找和字典种类,保留 VM 记录的原粒度。 // 两个检查独立执行,因此同次观察可能分别增加计数,后者诊断可覆盖前者。 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 分支表示字典种类观察,仍需保留其失效事实。 _ => "", }; 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, }); } /// 按尺寸、内容或绘制失效原因清理对应缓存,避免把所有变化都当成相同重建。 /// calls::Invalidate 已校验 reason,并按最终共享 Data 收集 affected 中的全部 Block 别名;这里不再自行扫描其他窗口。 pub fn invalidate( &mut self, cx: &mut NativeContext<'_>, affected: &std::collections::HashSet, 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::(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, ) -> ( std::collections::BTreeMap, Vec, ) { // 整体对象观察按身份归并到有序表,后续 observe_changes 比较该对象的当前版本。 let mut objects = std::collections::BTreeMap::new(); // 其他精确依赖保持原枚举与顺序,不能降级为对象版本而丢掉名字解析语义。 let mut names = Vec::new(); // 整对象依赖按身份收集到有序表,重复项以最后一次版本为准;其余精确依赖保持原记录。 // 调用方先排除各窗口 desktop 记账读取;本函数不负责过滤宿主身份,也不替调用方取得新的版本。 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()); } }