//! 一个原生窗口对应一份 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, pub pending: Option>, pub last_snapshot: Option>, 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, 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) { 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()); } 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> { 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()); } 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(()) } /// 构造本类型并初始化其状态;资源建立和默认字段以函数体为准。 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] fn independent_view_geometry_and_shared_vm_reads_do_not_add_roots() { 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, 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()); } }