//! Managed presentation contexts. The active desktop owns the mount dictionary; //! reverse links and the host registry are weak, so shared content cannot retain a Page. use crate::document::{Frame, MountGeometry}; use endor_core::{ ContextNativeFunction, NativeCallable, NativeContext, NativeResult, ObjectPayload, ObjectRef, Value, Vm, VmError, WeakObjectRef, }; use std::{ collections::HashMap, sync::{Arc, Mutex}, }; #[derive(Debug, Clone)] pub struct Mount { pub block: ObjectRef, pub page: Option, pub parent: Option, pub first_child: Option, pub next_sibling: Option, pub geometry: MountGeometry, pub version: u64, pub mounted: bool, // Private derived scratch, installed before the read-only layout call and // cleared by geometry commit. It contains no managed references or roots. pub layout: Option>>, } impl ObjectPayload for Mount { fn reference_assignment_only(&self) -> bool { true } fn as_any(&self) -> &dyn std::any::Any { self } fn kind_name(&self) -> &'static str { "tessera-mount" } fn allows_attributes(&self) -> bool { true } fn trace(&self, visit: &mut dyn FnMut(ObjectRef)) { visit(self.block); } } #[derive(Debug, Clone, Copy)] enum Query { Bounds, Parent, FirstChild, NextSibling, Page, Focus, HitTest, } #[derive(Debug)] struct Method { owner: ObjectRef, desktop: WeakObjectRef, query: Query, } impl NativeCallable for Method { fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result { let mount = cx.store().payload::(self.owner, "Mount")?.clone(); if !mount.mounted && matches!(self.query, Query::Bounds | Query::Focus | Query::HitTest) { return Err(VmError::InvalidArgument("Mount is no longer attached")); } let arity = if matches!(self.query, Query::HitTest) { 2 } else { 0 }; if args.len() != arity { return Err(VmError::InvalidArgument("incorrect Mount method arity")); } let value = match self.query { Query::Bounds => { let geometry = if let Some(layout) = &mount.layout { layout .lock() .map_err(|_| VmError::InvalidArgument("layout context poisoned"))? .bounds .ok_or(VmError::InvalidArgument( "Mount geometry is unavailable during measurement", ))? } else { [ mount.geometry.x, mount.geometry.y, mount.geometry.width, mount.geometry.height, ] }; let bounds = cx.new_object()?; for (name, value) in [ ("x", geometry[0]), ("y", geometry[1]), ("width", geometry[2]), ("height", geometry[3]), ] { cx.set_attr(bounds, name, Value::Number((value as f64).into()))?; } cx.set_attr( bounds, "layoutVersion", Value::Number((mount.version as i64).into()), )?; Value::Object(bounds) } Query::Focus => { if !mount.geometry.interactive { return Err(VmError::InvalidArgument("Mount is not focusable")); } let desktop = cx .upgrade(self.desktop)? .ok_or(VmError::InvalidArgument("Mount window has expired"))?; cx.set_attr( desktop, "focus", Value::Number((mount.geometry.id as i64).into()), )?; cx.set_attr(desktop, "focusRequested", Value::Bool(true))?; Value::Nil } Query::HitTest => { if mount.layout.is_some() { return Err(VmError::InvalidArgument( "Mount hitTest is unavailable during layout", )); } let number = |value: &Value| -> Result { let Value::Number(number) = value else { return Err(VmError::InvalidArgument( "point coordinates must be numbers", )); }; let number = number .to_string() .parse::() .map_err(|_| VmError::InvalidArgument("invalid point coordinate"))?; if !number.is_finite() { return Err(VmError::InvalidArgument("invalid point coordinate")); } Ok(number) }; Value::Bool( crate::geometry::Rect::new( mount.geometry.x, mount.geometry.y, mount.geometry.width, mount.geometry.height, ) .contains(crate::geometry::Point::new( number(&args[0])?, number(&args[1])?, )), ) } query => { let weak = match query { Query::Parent => mount.parent, Query::FirstChild => mount.first_child, Query::NextSibling => mount.next_sibling, Query::Page => mount.page, _ => unreachable!(), }; weak.map(|weak| cx.upgrade(weak)) .transpose()? .flatten() .map(Value::Object) .unwrap_or(Value::Nil) } }; Ok(NativeResult::Return(value)) } } #[derive(Debug, Default, Clone)] pub struct MountTable { pub active: HashMap, order: Vec, } #[derive(Debug)] struct Sync { desktop: ObjectRef, page: Option, nodes: Vec, old: HashMap, old_order: Vec, frame: Frame, preparing: bool, output: Arc>>, } #[derive(Debug)] struct LifecycleCall { function: ObjectRef, block: ObjectRef, mount: ObjectRef, } impl NativeCallable for LifecycleCall { fn invoke(&self, _: &mut NativeContext<'_>, _: &[Value]) -> Result { Ok(NativeResult::Invoke { function: self.function, arguments: vec![Value::Object(self.block), Value::Object(self.mount)], }) } } fn lifecycle_call( cx: &mut NativeContext<'_>, mount: ObjectRef, name: &'static str, ) -> Result { let block = cx.store().payload::(mount, "Mount")?.block; let definition = cx .store() .type_of(block)? .ok_or(VmError::InvalidArgument("untyped mounted Block"))?; let function = cx .store() .lookup_method(definition, name)? .ok_or(VmError::InvalidArgument("Block has no lifecycle method"))?; cx.allocate(ContextNativeFunction::new( name, LifecycleCall { function, block, mount, }, vec![function, block, mount], )) } impl NativeCallable for Sync { fn invoke(&self, cx: &mut NativeContext<'_>, _args: &[Value]) -> Result { let mut blocks = HashMap::new(); for weak in &self.nodes { if let Some(block) = cx.upgrade(*weak)? { blocks.insert(block.to_string(), block); } } let page = self .page .map(|page| cx.store().downgrade(page)) .transpose()?; let mut objects = HashMap::new(); let mut created = Vec::new(); for geometry in &self.frame.mounts { let block = *blocks .get(&geometry.block_id) .ok_or(VmError::InvalidArgument( "frame references an unavailable Block", ))?; let old = self .old .get(&geometry.id) .map(|weak| cx.upgrade(*weak)) .transpose()? .flatten(); let reusable = if let Some(old) = old { let mount = cx.store().payload::(old, "Mount")?; (mount.mounted && mount.page == page && mount.block == block).then_some(old) } else { None }; let object = if let Some(old) = reusable { old } else { let object = cx.allocate(Mount { block, page, parent: None, first_child: None, next_sibling: None, geometry: geometry.clone(), version: self.frame.layout_version, mounted: true, layout: None, })?; crate::ui_bridge::attach_desktop_type(cx, object, "Mount", self.desktop)?; for (name, query) in [ ("bounds", Query::Bounds), ("parent", Query::Parent), ("firstChild", Query::FirstChild), ("nextSibling", Query::NextSibling), ("page", Query::Page), ("requestFocus", Query::Focus), ("hitTest", Query::HitTest), ] { let function = cx.allocate(ContextNativeFunction::new( name, Method { owner: object, desktop: cx.store().downgrade(self.desktop)?, query, }, vec![object], ))?; cx.set_attr(object, name, Value::Object(function))?; } cx.set_attr(object, "id", Value::Number((geometry.id as i64).into()))?; cx.set_attr(object, "block", Value::Object(block))?; created.push(object); object }; objects.insert(geometry.id, object); } let root = cx.new_dict()?; for geometry in &self.frame.mounts { let object = objects[&geometry.id]; let link = |id: Option| -> Result, VmError> { id.map(|id| cx.store().downgrade(objects[&id])).transpose() }; let mount = Mount { block: blocks[&geometry.block_id], page, parent: link(geometry.parent)?, first_child: link(geometry.first_child)?, next_sibling: link(geometry.next_sibling)?, geometry: geometry.clone(), version: self.frame.layout_version, mounted: true, layout: if self.preparing { Some(Arc::new(Mutex::new(crate::layout::Context::default()))) } else { None }, }; let old = cx.store().payload::(object, "Mount")?; if self.preparing || old.layout.is_some() || !old.mounted || old.version != mount.version || old.geometry != mount.geometry || old.page != mount.page || old.parent != mount.parent || old.first_child != mount.first_child || old.next_sibling != mount.next_sibling || matches!( cx.store().get_attr(object, "mounted"), Err(VmError::AttributeNotFound(_)) ) { cx.replace_payload(object, mount)?; cx.set_attr(object, "mounted", Value::Bool(true))?; cx.set_attr( object, "layoutVersion", Value::Number((self.frame.layout_version as i64).into()), )?; } cx.dict_set( root, Value::Text(geometry.id.to_string()), Value::Object(object), )?; } let mut hooks = Vec::new(); // Layout records are preorder; reversing guarantees children unmount // before their parents. A Page change creates fresh Mount contexts. for id in self.old_order.iter().rev() { let weak = self.old[id]; if let Some(object) = cx.upgrade(weak)? && objects.get(id) != Some(&object) { let mut mount = cx.store().payload::(object, "Mount")?.clone(); mount.mounted = false; mount.layout = None; mount.parent = None; mount.first_child = None; mount.next_sibling = None; cx.replace_payload(object, mount)?; cx.set_attr(object, "mounted", Value::Bool(false))?; hooks.push(lifecycle_call(cx, object, "onUnmount")?); } } for object in created { hooks.push(lifecycle_call(cx, object, "onMount")?); } let driver = if hooks.is_empty() { None } else { Some(crate::vm_sequence::build( cx, hooks, "", self.desktop, )?) }; let weak = objects .into_iter() .map(|(id, object)| Ok((id, cx.store().downgrade(object)?))) .collect::, VmError>>()?; cx.set_attr(self.desktop, "mounts", Value::Object(root))?; *self .output .lock() .map_err(|_| VmError::InvalidArgument("Mount registry poisoned"))? = weak; Ok(NativeResult::Return( driver.map(Value::Object).unwrap_or(Value::Nil), )) } } impl MountTable { pub fn ordered(&self) -> Vec { self.order .iter() .filter_map(|id| self.active.get(id).copied()) .collect() } pub fn synchronize( &mut self, vm: &mut Vm, desktop: ObjectRef, page: Option, nodes: Vec, frame: &Frame, preparing: bool, ) -> Result, VmError> { let output = Arc::new(Mutex::new(HashMap::new())); let mut captures = vec![desktop]; captures.extend(page); let function = vm.allocate(ContextNativeFunction::new( "mount-sync", Sync { desktop, page, nodes, old: self.active.clone(), old_order: self.order.clone(), frame: frame.clone(), preparing, output: output.clone(), }, captures, ))?; let result = vm.call(function, &[]); vm.release(function)?; let result = result?; self.active = std::mem::take( &mut *output .lock() .map_err(|_| VmError::InvalidArgument("Mount registry poisoned"))?, ); self.order = frame.mounts.iter().map(|mount| mount.id).collect(); match result { Value::Object(hooks) => Ok(Some(hooks)), Value::Nil => Ok(None), value => { vm.release_value(value)?; Err(VmError::InvalidArgument("invalid lifecycle batch")) } } } } pub use crate::object_list::collection;