//! 一个原生窗口对应一份 Presentation:独立视口、Mount、测量缓存、焦点与依赖记录。 //! 多个窗口仍使用同一个 VM 和共享业务对象。读取版本失效时只标记相关展示需要刷新,不能因此复制一份业务状态。 //! //! 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, pub tabs: ObjectRef, pub active: Option, // 双层 Option 区分三态:None 无请求,Some(None) 请求清空,Some(Some(page)) 请求切页。 pub pending: Option>, pub last_snapshot: Option>, // 弱身份到原生稳定标签 ID 的映射;不能因为标签历史而保活已经关闭的页面。 pub tab_ids: Vec<(WeakObjectRef, u64)>, pub next_tab: u64, pub last_tabs: Vec, pub surface: crate::page::Surface, pub render_cache: crate::snapshot::RenderCache, pub layout: crate::layout::LayoutRuntime, pub mounts: crate::mount::MountTable, pub published_version: u64, // 即使根快照身份未变也要求重新发布一帧,例如源绘制读取的数据发生变化时。 pub force_frame: bool, // 精确名称/字典种类依赖,与下面按整个对象版本记录的 reads 分开检查。 pub paint_names: Vec, pub layout_names: Vec, pub paint_reads: std::collections::BTreeMap, pub dependency_invalidations: usize, pub last_invalid_dependency: String, pub layout_reads: std::collections::BTreeMap, pub measure_cache: crate::source_layout::Cache, pub snapshot_visited: usize, 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); 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); let name = match dependency { endor_core::ReadDependency::Name { name, .. } => name.as_str(), _ => "", }; 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, 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 在该窗口有活动出现时强制刷新,避免无关窗口重复发帧。 for weak in self.mounts.active.values() { 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 { 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, ) { let mut objects = std::collections::BTreeMap::new(); let mut names = Vec::new(); // 整对象依赖按身份收集到有序表,重复项以最后一次版本为准;其余精确依赖保持原记录。 for dependency in reads { match dependency { endor_core::ReadDependency::Object { object, version } => { objects.insert(object, version); } 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(); 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()); left.layout.viewport = Some(Size { width: 100.0, height: 80.0, }); right.layout.viewport = Some(Size { width: 200.0, height: 160.0, }); 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(); 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); left.layout.clear_occurrences(); let new_left = left.layout.frame(&root).unwrap(); 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); let shared = vm.new_object().unwrap(); let version = vm.change_version(shared).unwrap(); left.paint_reads.insert(shared, version); right.layout_reads.insert(shared, version); vm.set_attr(shared, "value", endor_core::Value::Bool(true)) .unwrap(); let changes = vm.take_changes(); left.observe_changes(&vm, &changes); right.observe_changes(&vm, &changes); assert!(left.force_frame && right.force_frame); // 最后释放业务对象并回收:两份读取记录都不应成为托管根,弱引用应升级失败。 let weak = vm.downgrade(shared).unwrap(); vm.release(shared).unwrap(); vm.collect_cycles(); assert!(vm.upgrade(weak).unwrap().is_none()); } }