//! 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)] 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 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_reads: std::collections::BTreeMap, 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 self .paint_reads .iter() .chain(self.layout_reads.iter()) .any(|(object, version)| vm.change_version(*object).ok() != Some(*version)) { self.force_frame = true; } 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(), render_cache: crate::snapshot::RenderCache::default(), layout: crate::layout::LayoutRuntime::default(), mounts: crate::mount::MountTable::default(), published_version: 0, force_frame: false, paint_reads: std::collections::BTreeMap::new(), layout_reads: std::collections::BTreeMap::new(), measure_cache: crate::source_layout::Cache::default(), snapshot_visited: 0, snapshot_reused: 0, } } } #[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 { flex: 0.0, flex_policy: crate::layout::FlexPolicy::default(), main_align: crate::layout::MainAlign::Start, 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()); } }